- New "admin dashboard" /admin page + login, #494

- New ESI scope for admin access
- New admin.log file for admin actions (kick, ban,..)
- New login status for characters
- improved cronJob exec time for systemData import (jump/kill data)
- Added PHP 64-bit check to /setup
This commit is contained in:
Exodus4D
2017-05-27 14:09:12 +02:00
parent cc4de64673
commit 5be1d3547a
44 changed files with 1428 additions and 437 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,246 @@
<?php
/**
* Created by PhpStorm.
* User: exodu
* Date: 12.05.2017
* Time: 20:30
*/
namespace Controller;
use Controller\Ccp\Sso;
use Model\BasicModel;
use Model\CharacterModel;
use Model\CorporationModel;
class Admin extends Controller{
const ERROR_SSO_CHARACTER_EXISTS = 'No character found. Please login first.';
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"';
const KICK_OPTIONS = [
5 => '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);
}
}

View File

@@ -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);
}
/**

View File

@@ -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);
}
/**

View File

@@ -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);
}
/**

View File

@@ -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);
}
/**

View File

@@ -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');

View File

@@ -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{

View File

@@ -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){

View File

@@ -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']);
}
/**

View File

@@ -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 ));
}
/**

View File

@@ -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'),

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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;
}
/**

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 },

View File

@@ -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($('<div/>', {'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'
}
});
});
});

View File

@@ -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);
};

View File

@@ -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 = $('<span>', {
let getLabel = function(text, options){
let label = $('<span>', {
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( $('<h5>').text(i + 'h ago'));
var killMailData = killboardData.tableData[i].killmails;
let killMailData = killboardData.tableData[i].killmails;
var listeElement = $('<ul>', {
let listeElement = $('<ul>', {
class: ['media-list', config.systemKillboardListClass].join(' ')
});
for(var j = 0; j < killMailData.length; j++){
for(let j = 0; j < killMailData.length; j++){
killMailCounter++;
if(killMailCounter >= killMailCounterMax){
break;
}
var killData = killMailData[j];
let killData = killMailData[j];
var linkUrl = 'https://zkillboard.com/kill/' + killData.killID + '/';
var victimImageUrl = 'https://image.eveonline.com/Type/' + killData.victim.shipTypeID + '_64.png';
var killDate = getDateObjectByTimeString(killData.killTime);
var killDateString = Util.convertDateToString(killDate);
var killLossValue = Util.formatPrice( killData.zkb.totalValue );
let linkUrl = '//zkillboard.com/kill/' + killData.killID + '/';
let victimImageUrl = Init.url.ccpImageServer + 'Type/' + killData.victim.shipTypeID + '_64.png';
let killDate = getDateObjectByTimeString(killData.killTime);
let killDateString = Util.convertDateToString(killDate);
let killLossValue = Util.formatPrice( killData.zkb.totalValue );
// check for ally
var victimAllyLogoUrl = '';
var displayAlly = 'none';
let victimAllyLogoUrl = '';
let displayAlly = 'none';
if(killData.victim.allianceID > 0){
victimAllyLogoUrl = 'https://image.eveonline.com/Alliance/' + killData.victim.allianceID + '_32.png';
victimAllyLogoUrl = Init.url.ccpImageServer + 'Alliance/' + killData.victim.allianceID + '_32.png';
displayAlly = 'block';
}
// check for corp
var victimCorpLogoUrl = '';
var displayCorp = 'none';
let victimCorpLogoUrl = '';
let displayCorp = 'none';
if(killData.victim.corporationID > 0){
victimCorpLogoUrl = 'https://image.eveonline.com/Corporation/' + killData.victim.corporationID + '_32.png';
victimCorpLogoUrl = Init.url.ccpImageServer + 'Corporation/' + killData.victim.corporationID + '_32.png';
displayCorp = 'inline';
}
var liElement = $('<li>', {
let liElement = $('<li>', {
class: ['media', config.systemKillboardListEntryClass].join(' ')
}).append(
$('<a>', {
@@ -180,11 +179,11 @@ define([
*/
$.fn.updateSystemInfoGraphs = function(systemData){
var moduleElement = $(this);
let moduleElement = $(this);
// headline toolbar icons
var headlineToolbar = $('<h5>', {
let headlineToolbar = $('<h5>', {
class: 'pull-right'
}).append(
$('<i>', {
@@ -192,7 +191,7 @@ define([
title: 'zkillboard.com'
}).on('click', function(e){
window.open(
'https://zkillboard.com/system/' + systemData.systemId,
'//zkillboard.com/system/' + systemData.systemId,
'_blank'
);
}).attr('data-toggle', 'tooltip')
@@ -201,30 +200,30 @@ define([
moduleElement.append(headlineToolbar);
// headline
var headline = $('<h5>', {
let headline = $('<h5>', {
text: 'Killboard'
});
moduleElement.append(headline);
var killboardGraphElement = $('<div>', {
let killboardGraphElement = $('<div>', {
class: config.systemKillboardGraphKillsClass
});
moduleElement.append(killboardGraphElement);
var showHours = 24;
var maxKillmailCount = 200; // limited by API
let showHours = 24;
let maxKillmailCount = 200; // limited by API
var labelOptions = {
let labelOptions = {
align: 'center-block'
};
var label = '';
let label = '';
// private function draws a "system kills" graph
var drawGraph = function(data){
let drawGraph = function(data){
var tableData = data.tableData;
let tableData = data.tableData;
// change order (show right to left)
tableData.reverse();
@@ -240,7 +239,7 @@ define([
return;
}
var labelYFormat = function(y){
let labelYFormat = function(y){
return Math.round(y);
};
@@ -287,10 +286,10 @@ define([
};
// get recent KB stats (last 24h))
var localDate = new Date();
let localDate = new Date();
// cache result for 5min
var cacheKey = systemData.systemId + '_' + localDate.getHours() + '_' + ( Math.ceil( localDate.getMinutes() / 5 ) * 5);
let cacheKey = systemData.systemId + '_' + localDate.getHours() + '_' + ( Math.ceil( localDate.getMinutes() / 5 ) * 5);
if(cache.systemKillsGraphData.hasOwnProperty(cacheKey) ){
// cached results
@@ -302,10 +301,10 @@ define([
}else{
// chart data
var chartData = [];
let chartData = [];
for(var i = 0; i < showHours; i++){
var tempData = {
for(let i = 0; i < showHours; i++){
let tempData = {
label: i + 'h',
kills: 0
};
@@ -314,18 +313,18 @@ define([
}
// get kills within the last 24h
var timeFrameInSeconds = 60 * 60 * 24;
let timeFrameInSeconds = 60 * 60 * 24;
// get current server time
var serverDate= Util.getServerTime();
let serverDate= Util.getServerTime();
// if system is w-space system -> add link modifier
var wSpaceLinkModifier = '';
let wSpaceLinkModifier = '';
if(systemData.type.id === 1){
wSpaceLinkModifier = 'w-space/';
}
var url = Init.url.zKillboard;
let url = Init.url.zKillboard;
url += 'no-items/' + wSpaceLinkModifier + 'no-attackers/solarSystemID/' + systemData.systemId + '/pastSeconds/' + timeFrameInSeconds + '/';
killboardGraphElement.showLoadingAnimation();
@@ -337,18 +336,18 @@ define([
}).done(function(kbData) {
// the API wont return more than 200KMs ! - remember last bar block with complete KM information
var lastCompleteDiffHourData = 0;
let lastCompleteDiffHourData = 0;
// loop kills and count kills by hour
for (var i = 0; i < kbData.length; i++) {
var killmailData = kbData[i];
for (let i = 0; i < kbData.length; i++) {
let killmailData = kbData[i];
var killDate = getDateObjectByTimeString(killmailData.killTime);
let killDate = getDateObjectByTimeString(killmailData.killTime);
// get time diff
var timeDiffMin = Math.round(( serverDate - killDate ) / 1000 / 60);
var timeDiffHour = Math.round(timeDiffMin / 60);
let timeDiffMin = Math.round(( serverDate - killDate ) / 1000 / 60);
let timeDiffHour = Math.round(timeDiffMin / 60);
// update chart data
if (chartData[timeDiffHour]) {
@@ -401,7 +400,7 @@ define([
// init tooltips
var tooltipElements = moduleElement.find('[data-toggle="tooltip"]');
let tooltipElements = moduleElement.find('[data-toggle="tooltip"]');
tooltipElements.tooltip({
container: 'body'
});
@@ -412,7 +411,7 @@ define([
* minify the killboard graph element e.g. if no kills where found, or on error
* @param killboardGraphElement
*/
var minifyKillboardGraphElement = function(killboardGraphElement){
let minifyKillboardGraphElement = function(killboardGraphElement){
killboardGraphElement.velocity({
height: '20px',
marginBottom: '0px'
@@ -426,9 +425,9 @@ define([
* @param timeString
* @returns {Date}
*/
var getDateObjectByTimeString = function(timeString){
var match = timeString.match(/^(\d+)-(\d+)-(\d+) (\d+)\:(\d+)\:(\d+)$/);
var date = new Date(match[1], match[2] - 1, match[3], match[4], match[5], match[6]);
let getDateObjectByTimeString = function(timeString){
let match = timeString.match(/^(\d+)-(\d+)-(\d+) (\d+)\:(\d+)\:(\d+)$/);
let date = new Date(match[1], match[2] - 1, match[3], match[4], match[5], match[6]);
return date;
};
@@ -438,10 +437,10 @@ define([
* @param systemData
* @returns {*|HTMLElement}
*/
var getModule = function(parentElement, systemData){
let getModule = function(parentElement, systemData){
// create new module container
var moduleElement = $('<div>', {
let moduleElement = $('<div>', {
class: [config.moduleClass, config.systemKillboardModuleClass].join(' '),
css: {opacity: 0}
});
@@ -461,10 +460,10 @@ define([
*/
$.fn.drawSystemKillboardModule = function(systemData){
var parentElement = $(this);
let parentElement = $(this);
// show route module
var showModule = function(moduleElement){
let showModule = function(moduleElement){
if(moduleElement){
moduleElement.velocity('transition.slideDownIn', {
duration: Init.animationSpeed.mapModule,
@@ -474,7 +473,7 @@ define([
};
// check if module already exists
var moduleElement = parentElement.find('.' + config.systemKillboardModuleClass);
let moduleElement = parentElement.find('.' + config.systemKillboardModuleClass);
if(moduleElement.length > 0){
moduleElement.velocity('transition.slideDownOut', {

File diff suppressed because one or more lines are too long

View File

@@ -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 },

View File

@@ -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($('<div/>', {'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'
}
});
});
});

View File

@@ -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);
};

View File

@@ -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 = $('<span>', {
let getLabel = function(text, options){
let label = $('<span>', {
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( $('<h5>').text(i + 'h ago'));
var killMailData = killboardData.tableData[i].killmails;
let killMailData = killboardData.tableData[i].killmails;
var listeElement = $('<ul>', {
let listeElement = $('<ul>', {
class: ['media-list', config.systemKillboardListClass].join(' ')
});
for(var j = 0; j < killMailData.length; j++){
for(let j = 0; j < killMailData.length; j++){
killMailCounter++;
if(killMailCounter >= killMailCounterMax){
break;
}
var killData = killMailData[j];
let killData = killMailData[j];
var linkUrl = 'https://zkillboard.com/kill/' + killData.killID + '/';
var victimImageUrl = 'https://image.eveonline.com/Type/' + killData.victim.shipTypeID + '_64.png';
var killDate = getDateObjectByTimeString(killData.killTime);
var killDateString = Util.convertDateToString(killDate);
var killLossValue = Util.formatPrice( killData.zkb.totalValue );
let linkUrl = '//zkillboard.com/kill/' + killData.killID + '/';
let victimImageUrl = Init.url.ccpImageServer + 'Type/' + killData.victim.shipTypeID + '_64.png';
let killDate = getDateObjectByTimeString(killData.killTime);
let killDateString = Util.convertDateToString(killDate);
let killLossValue = Util.formatPrice( killData.zkb.totalValue );
// check for ally
var victimAllyLogoUrl = '';
var displayAlly = 'none';
let victimAllyLogoUrl = '';
let displayAlly = 'none';
if(killData.victim.allianceID > 0){
victimAllyLogoUrl = 'https://image.eveonline.com/Alliance/' + killData.victim.allianceID + '_32.png';
victimAllyLogoUrl = Init.url.ccpImageServer + 'Alliance/' + killData.victim.allianceID + '_32.png';
displayAlly = 'block';
}
// check for corp
var victimCorpLogoUrl = '';
var displayCorp = 'none';
let victimCorpLogoUrl = '';
let displayCorp = 'none';
if(killData.victim.corporationID > 0){
victimCorpLogoUrl = 'https://image.eveonline.com/Corporation/' + killData.victim.corporationID + '_32.png';
victimCorpLogoUrl = Init.url.ccpImageServer + 'Corporation/' + killData.victim.corporationID + '_32.png';
displayCorp = 'inline';
}
var liElement = $('<li>', {
let liElement = $('<li>', {
class: ['media', config.systemKillboardListEntryClass].join(' ')
}).append(
$('<a>', {
@@ -180,11 +179,11 @@ define([
*/
$.fn.updateSystemInfoGraphs = function(systemData){
var moduleElement = $(this);
let moduleElement = $(this);
// headline toolbar icons
var headlineToolbar = $('<h5>', {
let headlineToolbar = $('<h5>', {
class: 'pull-right'
}).append(
$('<i>', {
@@ -192,7 +191,7 @@ define([
title: 'zkillboard.com'
}).on('click', function(e){
window.open(
'https://zkillboard.com/system/' + systemData.systemId,
'//zkillboard.com/system/' + systemData.systemId,
'_blank'
);
}).attr('data-toggle', 'tooltip')
@@ -201,30 +200,30 @@ define([
moduleElement.append(headlineToolbar);
// headline
var headline = $('<h5>', {
let headline = $('<h5>', {
text: 'Killboard'
});
moduleElement.append(headline);
var killboardGraphElement = $('<div>', {
let killboardGraphElement = $('<div>', {
class: config.systemKillboardGraphKillsClass
});
moduleElement.append(killboardGraphElement);
var showHours = 24;
var maxKillmailCount = 200; // limited by API
let showHours = 24;
let maxKillmailCount = 200; // limited by API
var labelOptions = {
let labelOptions = {
align: 'center-block'
};
var label = '';
let label = '';
// private function draws a "system kills" graph
var drawGraph = function(data){
let drawGraph = function(data){
var tableData = data.tableData;
let tableData = data.tableData;
// change order (show right to left)
tableData.reverse();
@@ -240,7 +239,7 @@ define([
return;
}
var labelYFormat = function(y){
let labelYFormat = function(y){
return Math.round(y);
};
@@ -287,10 +286,10 @@ define([
};
// get recent KB stats (last 24h))
var localDate = new Date();
let localDate = new Date();
// cache result for 5min
var cacheKey = systemData.systemId + '_' + localDate.getHours() + '_' + ( Math.ceil( localDate.getMinutes() / 5 ) * 5);
let cacheKey = systemData.systemId + '_' + localDate.getHours() + '_' + ( Math.ceil( localDate.getMinutes() / 5 ) * 5);
if(cache.systemKillsGraphData.hasOwnProperty(cacheKey) ){
// cached results
@@ -302,10 +301,10 @@ define([
}else{
// chart data
var chartData = [];
let chartData = [];
for(var i = 0; i < showHours; i++){
var tempData = {
for(let i = 0; i < showHours; i++){
let tempData = {
label: i + 'h',
kills: 0
};
@@ -314,18 +313,18 @@ define([
}
// get kills within the last 24h
var timeFrameInSeconds = 60 * 60 * 24;
let timeFrameInSeconds = 60 * 60 * 24;
// get current server time
var serverDate= Util.getServerTime();
let serverDate= Util.getServerTime();
// if system is w-space system -> add link modifier
var wSpaceLinkModifier = '';
let wSpaceLinkModifier = '';
if(systemData.type.id === 1){
wSpaceLinkModifier = 'w-space/';
}
var url = Init.url.zKillboard;
let url = Init.url.zKillboard;
url += 'no-items/' + wSpaceLinkModifier + 'no-attackers/solarSystemID/' + systemData.systemId + '/pastSeconds/' + timeFrameInSeconds + '/';
killboardGraphElement.showLoadingAnimation();
@@ -337,18 +336,18 @@ define([
}).done(function(kbData) {
// the API wont return more than 200KMs ! - remember last bar block with complete KM information
var lastCompleteDiffHourData = 0;
let lastCompleteDiffHourData = 0;
// loop kills and count kills by hour
for (var i = 0; i < kbData.length; i++) {
var killmailData = kbData[i];
for (let i = 0; i < kbData.length; i++) {
let killmailData = kbData[i];
var killDate = getDateObjectByTimeString(killmailData.killTime);
let killDate = getDateObjectByTimeString(killmailData.killTime);
// get time diff
var timeDiffMin = Math.round(( serverDate - killDate ) / 1000 / 60);
var timeDiffHour = Math.round(timeDiffMin / 60);
let timeDiffMin = Math.round(( serverDate - killDate ) / 1000 / 60);
let timeDiffHour = Math.round(timeDiffMin / 60);
// update chart data
if (chartData[timeDiffHour]) {
@@ -401,7 +400,7 @@ define([
// init tooltips
var tooltipElements = moduleElement.find('[data-toggle="tooltip"]');
let tooltipElements = moduleElement.find('[data-toggle="tooltip"]');
tooltipElements.tooltip({
container: 'body'
});
@@ -412,7 +411,7 @@ define([
* minify the killboard graph element e.g. if no kills where found, or on error
* @param killboardGraphElement
*/
var minifyKillboardGraphElement = function(killboardGraphElement){
let minifyKillboardGraphElement = function(killboardGraphElement){
killboardGraphElement.velocity({
height: '20px',
marginBottom: '0px'
@@ -426,9 +425,9 @@ define([
* @param timeString
* @returns {Date}
*/
var getDateObjectByTimeString = function(timeString){
var match = timeString.match(/^(\d+)-(\d+)-(\d+) (\d+)\:(\d+)\:(\d+)$/);
var date = new Date(match[1], match[2] - 1, match[3], match[4], match[5], match[6]);
let getDateObjectByTimeString = function(timeString){
let match = timeString.match(/^(\d+)-(\d+)-(\d+) (\d+)\:(\d+)\:(\d+)$/);
let date = new Date(match[1], match[2] - 1, match[3], match[4], match[5], match[6]);
return date;
};
@@ -438,10 +437,10 @@ define([
* @param systemData
* @returns {*|HTMLElement}
*/
var getModule = function(parentElement, systemData){
let getModule = function(parentElement, systemData){
// create new module container
var moduleElement = $('<div>', {
let moduleElement = $('<div>', {
class: [config.moduleClass, config.systemKillboardModuleClass].join(' '),
css: {opacity: 0}
});
@@ -461,10 +460,10 @@ define([
*/
$.fn.drawSystemKillboardModule = function(systemData){
var parentElement = $(this);
let parentElement = $(this);
// show route module
var showModule = function(moduleElement){
let showModule = function(moduleElement){
if(moduleElement){
moduleElement.velocity('transition.slideDownIn', {
duration: Init.animationSpeed.mapModule,
@@ -474,7 +473,7 @@ define([
};
// check if module already exists
var moduleElement = parentElement.find('.' + config.systemKillboardModuleClass);
let moduleElement = parentElement.find('.' + config.systemKillboardModuleClass);
if(moduleElement.length > 0){
moduleElement.velocity('transition.slideDownOut', {

View File

@@ -0,0 +1,32 @@
<section id="pf-landing-login">
<div class="container col-xs-12">
<div class="row text-center">
<div class="col-xs-12 col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-6 col-lg-offset-3 pf-landing-pricing-panel">
<div class="panel panel-default pricing-big pf-animate-on-visible pf-animate">
<div class="ribbon-wrapper"><div class="ribbon ribbon-orange">BETA</div></div>
<div class="panel-heading text-left">
<h3 class="panel-title">Admin access</h3>
</div>
<div class="panel-body no-padding">
<div class="price-features price-features-fluid">
<ul class="list-unstyled text-left">
<li><i class="fa fa-fw fa-caret-right"></i> Click the SSO button below for <em>PATHFINDER</em> access with admin roles</li>
<li><i class="fa fa-fw fa-caret-right"></i> Admin roles require at least on of these in-game corporation roles granted to your character</li>
<li>
<ul>
<li><i class="fa fa-fw fa-angle-right"></i> <kbd>director</kbd>, <kbd>personnel_manager</kbd> or <kbd>security_officer</kbd></li>
</ul>
</li>
<li><i class="fa fa-fw fa-caret-right"></i> The admin SSO requires additional <em>ESI</em> scopes to be accepted, in order to get in-game roles from <em>EVE</em></li>
</ul>
</div>
</div>
</div>
<include href="templates/modules/sso.html"/>
</div>
</div>
</div>
<div class="container"></div>
</section>

View File

@@ -0,0 +1,102 @@
<section>
<div class="container col-xs-12">
<div class="row"></div>
<h4><i class="fa fa-fw fa-users"></i>&nbsp;{{ ucfirst(@tplPage) }}</h4>
<div class="row text-center">
<div class="col-xs-12 pf-landing-pricing-panel">
<div class="panel panel-default pricing-big">
<div class="panel-heading text-left">
<h3 class="panel-title">Corporation</h3>
</div>
<div class="panel-body">
<table id="table_id" class="stripe order-column row-border dataTable" data-order="[1, &quot;asc&quot;]" data-length-change="0" data-paging-type="full_numbers">
<thead>
<tr>
<th data-width="50">id</th>
<th>name</th>
<th data-width="50">role</th>
<th data-width="50">share</th>
<th data-width="50">tracking</th>
<th data-width="50">security</th>
<th data-width="90">last login</th>
<th data-width="90">kicked until</th>
<th data-width="90" data-orderable="false">kick</th>
<th data-width="90">banned since</th>
<th data-width="30" data-orderable="false">ban</th>
</tr>
</thead>
<tbody>
<repeat group="{{ @tplMembers->members }}" value="{{ @member }}" counter="{{ @ctr }}">
<set disableRow = "{{@member->_id}} == {{@character->_id}}" />
<tr>
<td class="text-right">{{ @member->_id }}</td>
<td class="pf-table-button-sm-cell" data-order="{{ @member->name }}" data-search="{{ @member->name }}">
<img src="//image.eveonline.com/Character/{{ @member->_id }}_32.jpg" >&nbsp;{{ @member->name }}
</td>
<td class="text-right">
<check if="{{ @member->roleId }}">
<span class="label label-success">admin</span>
</check>
</td>
<td class="text-right">
<check if="{{ @member->shared }}">
<true>
<span class="label label-info">active</span>
</true>
<false>
<span class="label label-default">disabled</span>
</false>
</check>
</td>
<td class="text-right">
<check if="{{ @member->logLocation }}">
<true>
<span class="label label-success">active</span>
</true>
<false>
<span class="label label-warning">disabled</span>
</false>
</check>
</td>
<td class="text-right txt-color {{ @member->securityStatus >= 0 ? 'txt-color-green' : 'txt-color-orange' }}">{{ number_format(round(@member->securityStatus, 2), 2) }}</td>
<td class="text-right">{{ @member->getFormattedColumn('lastLogin') }}</td>
<td class="text-right txt-color txt-color-orange">{{ @member->getFormattedColumn('kicked') }}</td>
<td class="text-center pf-table-button-sm-cell">
<div class="btn-group btn-group-sm" role="group">
<check if="{{ @member->kicked }}">
<true>
<a class="btn btn-primary {{ @disableRow ? 'disabled' : '' }}" href="/admin/{{ @tplPage}}/kick/{{ @member->_id }}">revoke</a>
</true>
<false>
<repeat group="{{ @tplKickOptions }}" key="{{ @key }}" value="{{ @label }}">
<a class="btn btn-default {{ @disableRow ? 'disabled' : '' }}" href="/admin/{{ @tplPage}}/kick/{{ @member->_id }}/{{ @key }}">{{ @label }}</a>
</repeat>
</false>
</check>
</div>
</td>
<td class="text-right txt-color txt-color-danger">{{ @member->getFormattedColumn('banned') }}</td>
<td class="text-center pf-table-button-sm-cell">
<check if="{{ @member->banned }}">
<true>
<a class="btn btn-sm btn-primary {{ @disableRow ? 'disabled' : '' }}" href="/admin/{{ @tplPage}}/ban/{{ @member->_id }}">revoke</a>
</true>
<false>
<a class="btn btn-sm btn-danger {{ @disableRow ? 'disabled' : '' }}" href="/admin/{{ @tplPage}}/ban/{{ @member->_id }}/1">ban</a>
</false>
</check>
</td>
</tr>
</repeat>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,12 @@
<div class="row text-center">
<div class="col-xs-12 pf-landing-pricing-panel">
<div class="panel panel-default pricing-big">
<div class="panel-heading text-left">
<h3 class="panel-title">Settings</h3>
</div>
<div class="panel-body">
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
<check if="{{ @SESSION.SSO.ERROR }}">
<div class="container-fluid">
<div class="row ">
<div class="col-sm-8 col-sm-offset-2">
<div class="alert alert-danger" >
<span class="txt-color txt-color-danger">Access denied</span>
<small>{{ @SESSION.SSO.ERROR }}</small>
</div>
</div>
</div>
</div>
</check>
{* SSO login *}
<div class="container-fluid">
<div class="row text-center">
<div class="col-xs-12">
<a class="pf-sso-login-button {{@registrationStatusButton}}" target="_self" href="{{ @tplAuthType }}" type="button" tabindex="3" title="{{@registrationStatusTitle}}">&nbsp;</a><br>
<a class="font-lg" target="_blank" href="http://community.eveonline.com/news/dev-blogs/eve-online-sso-and-what-you-need-to-know">What is this?</a>
</div>
</div>
</div>
{* splash page -> shown by Javascript *}
<div id="pf-notification-panel"></div>

View File

@@ -0,0 +1,6 @@
<div id="pf-landing-admin-panel" class="hidden-xs pf-landing-sticky-panel">
<h4 class="text-center">Admin</h4>
<ul class="fa-ul">
<li><i class="fa-li fa fa-sign-in"></i><a href="{{ 'admin', ['*' => ''] | alias }}">login</a></li>
</ul>
</div>

View File

@@ -1,4 +1,15 @@
{{ character.authorizationStatus}}
<a href="{{ link }}?cookie={{ cookieName }}">
{{#authOK}}
{{#character.roleId}}
<div class="ribbon-wrapper fade in"><div class="ribbon ribbon-blue">ADMIN</div></div>
{{/character.roleId}}
{{/authOK}}
{{^authOK}}
<div class="ribbon-wrapper fade in"><div class="ribbon ribbon-red">{{authLabel}}</div></div>
{{/authOK}}
<div class="pf-character-image-wrapper">
<div class="pf-character-select-image">
<img class="pf-character-image" src="https://image.eveonline.com/Character/{{ character.id }}_128.jpg" alt="{{ character.name }}"/>

View File

@@ -1,17 +1,17 @@
<div id="{{ serverPanelId }}" class="hidden-xs">
<div id="{{ stickyPanelServerId }}" class="hidden-xs {{ stickyPanelClass }}">
<h4 class="text-center">{{ serverName }}</h4>
<ul class="fa-ul">
{{#serviceStatus}}
<li><i class="fa-li fa fa-server " aria-hidden="true"></i><span class="txt-color {{ style }}">{{ eve }}</span></li>
<li><i class="fa-li fa fa-server"></i><span class="txt-color {{ style }}">{{ eve }}</span></li>
{{/serviceStatus}}
{{#playerCount}}
<li><i class="fa-li fa fa-users" aria-hidden="true"></i>{{ playerCount }}</li>
<li><i class="fa-li fa fa-users"></i>{{ playerCount }}</li>
{{/playerCount}}
{{#startTime}}
<li><i class="fa-li fa fa-clock-o" aria-hidden="true"></i>up {{ startTime }}</li>
<li><i class="fa-li fa fa-clock-o"></i>up {{ startTime }}</li>
{{/startTime}}
{{#serverVersion}}
<li><i class="fa-li fa fa-certificate" aria-hidden="true"></i>v. {{ serverVersion }}</li>
<li><i class="fa-li fa fa-certificate"></i>v. {{ serverVersion }}</li>
{{/serverVersion}}
</ul>
</div>

View File

@@ -0,0 +1,32 @@
{* splash page *}
<include href="templates/ui/splash.html"/>
<nav id="pf-navbar" class="navbar navbar-default navbar-fixed-top pf-head">
<div class="container col-sm-12">
{* Logged in *}
<check if="{{ @tplLogged }}">
<div id="pf-head" class="navbar-header">
<span class="navbar-brand">
<img class="pull-left" style="width: 16px" src="https://image.eveonline.com/Corporation/{{ @character->corporationId->id }}_64.png"/>&nbsp;{{ @character->corporationId->name }}
</span>
<p class="navbar-text">
{{ @character->name }}
</p>
</div>
<div class="navbar-collapse">
<ul class="nav navbar-nav navbar-right" role="tablist">
<li class="{{ @tplPage == 'settings' ? 'active' : '' }}"><a href="/admin/settings">Settings</a></li>
<li class="{{ @tplPage == 'members' ? 'active' : '' }}"><a href="/admin/members"><i class="fa fa-fw fa-users"></i>&nbsp;Members</a></li>
<li class="{{ @tplPage == 'maps' ? 'active' : '' }}"><a href="/admin/maps">Maps</a></li>
<li class="{{ @tplPage == 'activity' ? 'active' : '' }}"><a href="/admin/activity">Activity</a></li>
<li class="{{ @tplPage == 'login' ? 'active' : '' }}"><a href="/admin/login"><i class="fa fa-fw fa-sign-in"></i>&nbsp;SSO</a></li>
</ul>
</div>
</check>
</div>
</nav>
<include if="{{ @tplPage }}" href="{{ 'templates/admin/' . @tplPage . '.html' }}" />

View File

@@ -27,7 +27,7 @@
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right" role="tablist">
<li><a class="page-scroll" data-anchor="#pf-landing-top" href="#">Home</a></li>
<li><a class="page-scroll" data-anchor="#pf-landing-top" href="#">Login</a></li>
<li> <a class="page-scroll" data-anchor="#pf-landing-gallery" href="#">Features</a></li>
<li> <a class="page-scroll" data-anchor="#pf-landing-maps" href="#">Maps</a></li>
<li> <a class="page-scroll" data-anchor="#pf-landing-install" href="#">Install</a></li>
@@ -41,6 +41,10 @@
{* header *}
<header id="pf-landing-top">
{* Admin login panel *}
<include href="templates/ui/admin_panel.html"/>
<div id="pf-header-container">
<canvas id="pf-header-canvas" class="hidden-xs" width="500" height="480"></canvas>
<div class="container">
@@ -113,29 +117,8 @@
</false>
</check>
{* login message container *}
<check if="{{ @SESSION.SSO.ERROR }}">
<div class="container-fluid">
<div class="row ">
<div class="col-sm-8 col-sm-offset-2">
<div class="alert alert-danger" >
<span class="txt-color txt-color-danger">Access denied</span>
<small>{{ @SESSION.SSO.ERROR }}</small>
</div>
</div>
</div>
</div>
</check>
{* SSO login *}
<div class="container-fluid">
<div class="row text-center">
<div class="col-xs-12">
<a class="pf-sso-login-button {{@registrationStatusButton}}" target="_self" href="{{ 'sso','action=requestAuthorization' | alias }}" type="button" tabindex="3" title="{{@registrationStatusTitle}}">&nbsp;</a><br>
<a class="font-lg" target="_blank" href="http://community.eveonline.com/news/dev-blogs/eve-online-sso-and-what-you-need-to-know">What is this?</a>
</div>
</div>
</div>
<include href="templates/modules/sso.html"/>
{* splash page -> shown by Javascript *}
<div id="pf-notification-panel"></div>
@@ -858,7 +841,7 @@
Feel free to contact me with your problem, either by submitting a <a target="_blank" href="https://github.com/exodus4d/pathfinder/issues">bug report</a> or contact me in game.
I´ll give my best to find a solution for your problem or path <em>Pathfinder</em>.
</p>
<h3>I don´t trust you, can I host <em>Pathfinder</em> on my own webserver?</h3>
<h3>I don´t trust you, can I host <em>Pathfinder</em> on my own?</h3>
<p>
Yes you can! I developed this application for the great community of <em>EVE Online</em>.
The program code is open source and can be used by anyone who have the required software skills.
@@ -870,10 +853,10 @@
</p>
<ul class="fa-ul pf-landing-list">
<li><i></i> A webserver with a <em><a target="_blank" href="https://en.wikipedia.org/wiki/LAMP_(software_bundle)">LAMP</a></em> environment</li>
<li><i></i> PHP 5.6+</li>
<li><i></i> PHP framework <a target="_blank" href="http://fatfreeframework.com/system-requirements">requirements</a> </li>
<li><i></i> MySQL 5.6+ </li>
<li><i></i> Some kind of server side caching technology (Memcache,APC,xCache) file caching will also work</li>
<li><i></i> PHP 7.0+</li>
<li><i></i> PHP framework <a target="_blank" href="http://fatfreeframework.com/system-requirements">requirements</a></li>
<li><i></i> MySQL 5.7+ </li>
<li><i></i> Some kind of server side caching. <em><a target="_blank" href="https://redis.io/">Redis</a></em> is preferred, file cache will also work</li>
</ul>
</div>
</div>

View File

@@ -11,5 +11,5 @@
@import "_timeline";
@import "_ribbon";
@import "_loading-bar";
@import "_server-panel";
@import "sticky-panel";
@import "_youtube";

View File

@@ -167,7 +167,7 @@
}
// header --------------------------------------------------------------------
#pf-landing-top{
height: 450px;
height: 355px;
border-bottom: 1px solid $gray-dark;
position: relative;
@@ -182,6 +182,10 @@
@include filter(brightness(0.9))
}
#pf-logo-container{
@include transform( scale3d(0.8, 0.8, 1) ); // downscales logo
}
#pf-header-container{
position: absolute;
width: 100%;
@@ -203,7 +207,7 @@
left: 400px;
width: 590px;
height: 350px;
top: 92px;
top: 37px;
.pf-header-preview-element{
position: relative;
@@ -241,7 +245,7 @@
.container{
position: relative;
margin-top: 50px;
margin-top: 10px;
}
}
@@ -301,9 +305,14 @@
padding: 10px 10px 5px 10px;
min-width: 155px;
min-height: 184px;
overflow: visible; // overwrite default
@include border-radius(10px);
@include box-shadow(0 4px 10px rgba(0,0,0, 0.4));
.ribbon-wrapper{
z-index: 5 ;
}
// character images
.pf-character-image-wrapper{
opacity: 0;
@@ -538,9 +547,12 @@
background: $gray;
color: $gray-lighter;
padding: 20px 15px;
min-height: 205px;
line-height: 22px;
&:not(.price-features-fluid){
min-height: 205px;
}
.list-unstyled.text-left li{
text-indent: -1em;
padding-left: 1.5em;

View File

@@ -252,6 +252,10 @@ select:active, select:hover {
}
}
&.pf-table-button-sm-cell{
padding: 0;
}
&.pf-table-counter-cell{
color: $gray-light;

View File

@@ -6,6 +6,7 @@
position: absolute;
top: -3px;
right: -3px;
pointer-events: none;
}
.ribbon {
@@ -54,9 +55,28 @@
@include background-image(linear-gradient(top, darken($orange, 3%), darken($orange-dark, 3%)));
&:before, &:after{
border-top: 3px solid darken($orange-dark, 18%);
border-top: 3px solid darken($orange-dark, 18%) ;
}
}
&.ribbon-red{
background-color: $red;
@include background-image(linear-gradient(top, darken($red, 10%), darken($red, 18%)));
&:before, &:after{
border-top: 3px solid darken($red, 38%);
}
}
&.ribbon-blue{
background-color: $blue;
@include background-image(linear-gradient(top, darken($blue, 3%), darken($blue-dark, 3%)));
&:before, &:after{
border-top: 3px solid darken($blue-dark, 18%);
}
}
}
.ribbon:before {

View File

@@ -1,8 +1,6 @@
#pf-server-panel{
.pf-landing-sticky-panel{
position: fixed;
top: 50px;
min-width: 100px;
left: 10px;
border-radius: 5px;
padding: 7px;
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
@@ -21,5 +19,14 @@
text-transform: lowercase;
}
}
}
#pf-landing-server-panel{
top: 50px;
left: 10px;
}
#pf-landing-admin-panel{
bottom: 45px;
right: 10px;
}