closed #138 added new cookie based login

This commit is contained in:
Exodus4D
2016-05-01 17:20:16 +02:00
parent 7f23e31984
commit 2b58853bfb
20 changed files with 1016 additions and 219 deletions

View File

@@ -19,9 +19,9 @@ class AccessController extends Controller {
function beforeroute(\Base $f3) {
parent::beforeroute($f3);
// Any CMS route of a child class of this one, requires a
// valid logged in user!
$loginCheck = $this->checkLogIn($f3);
// Any route/endpoint of a child class of this one,
// requires a valid logged in user!
$loginCheck = $this->checkLogTimer($f3);
if( !$loginCheck ){
// no user found or LogIn timer expired
@@ -29,32 +29,4 @@ class AccessController extends Controller {
}
}
/**
* checks weather a user is currently logged in
* @param \Base $f3
* @return bool
*/
private function checkLogIn($f3){
$loginCheck = false;
if($f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) > 0){
// check logIn time
$logInTime = new \DateTime();
$logInTime->setTimestamp( $f3->get(Api\User::SESSION_KEY_CHARACTER_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;
}
}

View File

@@ -568,7 +568,6 @@ class Route extends \Controller\AccessController {
$item = false;
}
$data[0]->reset();
}
}, [$map, $validMaps, $activeCharacter]);

View File

@@ -84,6 +84,35 @@ class User extends Controller\Controller{
return $login;
}
/**
* validate cookie character information
* -> return character data (if valid)
* @param \Base $f3
*/
public function getCookieCharacter($f3){
$data = $f3->get('POST');
$return = (object) [];
$return->error = [];
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))) ){
// character is valid and allowed to login
$return->character = reset($characters)->getData();
}else{
$characterError = (object) [];
$characterError->type = 'warning';
$characterError->message = 'This can happen through "invalid cookie data", "login restrictions", "CREST problems".';
$return->error[] = $characterError;
}
}
}
echo json_encode($return);
}
/**
* get captcha image and store key to session
* @param \Base $f3
@@ -149,64 +178,6 @@ class User extends Controller\Controller{
parent::logOut($f3);
}
/**
* save/update "map sharing" configurations for all map types
* the user has access to
* @param \Base $f3
*/
public function saveSharingConfig(\Base $f3){
$data = $f3->get('POST');
$return = (object) [];
$activeCharacter = $this->getCharacter();
if($activeCharacter){
$privateSharing = 0;
$corporationSharing = 0;
$allianceSharing = 0;
// form values
if(isset($data['formData'])){
$formData = $data['formData'];
if(isset($formData['privateSharing'])){
$privateSharing = 1;
}
if(isset($formData['corporationSharing'])){
$corporationSharing = 1;
}
if(isset($formData['allianceSharing'])){
$allianceSharing = 1;
}
}
$activeCharacter->shared = $privateSharing;
$activeCharacter = $activeCharacter->save();
// update corp/ally ---------------------------------------------------------------
$corporation = $activeCharacter->getCorporation();
$alliance = $activeCharacter->getAlliance();
if(is_object($corporation)){
$corporation->shared = $corporationSharing;
$corporation->save();
}
if(is_object($alliance)){
$alliance->shared = $allianceSharing;
$alliance->save();
}
$user = $activeCharacter->getUser();
$return->userData = $user->getData();
}
echo json_encode($return);
}
/**
* update user account data
* -> a fresh user automatically generated on first login with a new character

View File

@@ -8,6 +8,7 @@
namespace Controller;
use Controller\Ccp as Ccp;
use Model;
class AppController extends Controller {
@@ -43,6 +44,12 @@ class AppController extends Controller {
// JS main file
$f3->set('jsView', 'login');
// characters from cookies
$f3->set('cookieCharacters', $this->getCookieByName(self::COOKIE_PREFIX_CHARACTER, true));
$f3->set('getCharacterGrid', function($characters){
return ( ((12 / count($characters)) <= 4) ? 4 : (12 / count($characters)) );
});
}
}

View File

@@ -52,6 +52,7 @@ class Sso extends Api\User{
const ERROR_CHARACTER_FORBIDDEN = 'Character "%s" is not authorized to log in';
const ERROR_CHARACTER_MISMATCH = 'The character "%s" you tried to log in, does not match';
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';
/**
* CREST "Scopes" are used by pathfinder
@@ -156,7 +157,7 @@ class Sso extends Api\User{
// get character data from CREST
$characterData = $this->getCharacterData($accessData->accessToken);
if(isset($characterData->character)){
if( isset($characterData->character) ){
// add "ownerHash" and CREST tokens
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$characterData->character['crestAccessToken'] = $accessData->accessToken;
@@ -203,6 +204,9 @@ class Sso extends Api\User{
$loginCheck = $this->loginByCharacter($characterModel);
if($loginCheck){
// set "login" cookie
$this->setLoginCookie($characterModel);
// route to "map"
$f3->reroute('@map');
}else{
@@ -235,6 +239,38 @@ class Sso extends Api\User{
}
}
/**
* login by cookie
* @param \Base $f3
*/
public function login(\Base $f3){
$data = (array)$f3->get('GET');
$character = null;
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))) ){
// character is valid and allowed to login
$character = $characters[$data['cookie']];
}
}
}
if( is_object($character)){
// login by character
$loginCheck = $this->loginByCharacter($character);
if($loginCheck){
// route to "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');
}
/**
* get a valid "access_token" for oAuth 2.0 verification
* -> if $authCode is set -> request NEW "access_token"
@@ -357,7 +393,7 @@ class Sso extends Api\User{
* @param $accessToken
* @return mixed|null
*/
protected function verifyCharacterData($accessToken){
public function verifyCharacterData($accessToken){
$verifyUserUrl = self::getVerifyUserEndpoint();
$verifyUrlParts = parse_url($verifyUserUrl);
$characterData = null;
@@ -492,7 +528,7 @@ class Sso extends Api\User{
* @param array $additionalOptions
* @return object
*/
protected function getCharacterData($accessToken, $additionalOptions = []){
public function getCharacterData($accessToken, $additionalOptions = []){
$endpoints = $this->getEndpoints($accessToken, $additionalOptions);
$characterData = (object) [];

View File

@@ -13,6 +13,10 @@ use DB;
class Controller {
// cookie specific keys (names)
const COOKIE_NAME_STATE = 'cookie';
const COOKIE_PREFIX_CHARACTER = 'char';
/**
* @var \Base
*/
@@ -114,6 +118,200 @@ class Controller {
}
}
/**
* get cookies "state" information
* -> whether user accepts cookies
* @return bool
*/
protected function getCookieState(){
return (bool)count( $this->getCookieByName(self::COOKIE_NAME_STATE) );
}
/**
* search for existing cookies
* -> either a specific cookie by its name
* -> or get multiple cookies by their name (search by prefix)
* @param $cookieName
* @param bool $prefix
* @return array
*/
protected function getCookieByName($cookieName, $prefix = false){
$data = [];
if(!empty($cookieName)){
$cookieData = (array)$this->getF3()->get('COOKIE');
if($prefix === true){
// look for multiple cookies with same prefix
foreach($cookieData as $name => $value){
if(strpos($name, $cookieName) === 0){
$data[$name] = $value;
}
}
}elseif( isset($cookieData[$cookieName]) ){
// look for a single cookie
$data[$cookieName] = $cookieData[$cookieName];
}
}
return $data;
}
/**
* set/update logged in cookie by character model
* -> store validation data in DB
* @param Model\CharacterModel $character
*/
protected function setLoginCookie(Model\CharacterModel $character){
if( $this->getCookieState() ){
$expireSeconds = (int) $this->getF3()->get('PATHFINDER.LOGIN.COOKIE_EXPIRE');
$expireSeconds *= 24 * 60 * 60;
$timezone = new \DateTimeZone( $this->getF3()->get('TZ') );
$expireTime = new \DateTime('now', $timezone);
// add cookie expire time
$expireTime->add(new \DateInterval('PT' . $expireSeconds . 'S'));
// unique "selector" -> to facilitate database look-ups (small size)
// -> This is preferable to simply using the database id field,
// which leaks the number of active users on the application
$selector = bin2hex(mcrypt_create_iv(12, MCRYPT_DEV_URANDOM));
// generate unique "validator" (strong encryption)
// -> plaintext set to user (cookie), hashed version of this in DB
$size = mcrypt_get_iv_size(MCRYPT_CAST_256, MCRYPT_MODE_CFB);
$validator = bin2hex(mcrypt_create_iv($size, MCRYPT_DEV_URANDOM));
// generate unique cookie token
$token = hash('sha256', $validator);
// get unique cookie name for this character
$name = md5($character->name);
$authData = [
'characterId' => $character,
'selector' => $selector,
'token' => $token,
'expires' => $expireTime->format('Y-m-d H:i:s')
];
$authenticationModel = $character->rel('characterTokens');
$authenticationModel->copyfrom($authData);
$authenticationModel->save();
$cookieValue = implode(':', [$selector, $validator]);
// get cookie name -> save new one OR update existing cookie
$cookieName = 'COOKIE.' . self::COOKIE_PREFIX_CHARACTER . '_' . $name;
$this->getF3()->set($cookieName, $cookieValue, $expireSeconds);
}
}
/**
* get characters from given cookie data
* -> validate cookie data
* -> validate characters
* @param array $cookieData
* @return array
* @throws \Exception
*/
protected function getCookieCharacters($cookieData = []){
$characters = [];
if(
$this->getCookieState() &&
!empty($cookieData)
){
/**
* @var $characterAuth Model\CharacterAuthenticationModel
*/
$characterAuth = Model\BasicModel::getNew('CharacterAuthenticationModel');
$timezone = new \DateTimeZone( $this->getF3()->get('TZ') );
$currentTime = new \DateTime('now', $timezone);
foreach($cookieData as $name => $value){
// remove invalid cookies
$invalidCookie = false;
$data = explode(':', $value);
if(count($data) === 2){
// cookie data is well formatted
$characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1]);
// validate expire data
// validate token
if(
!$characterAuth->dry() &&
strtotime($characterAuth->expires) >= $currentTime->getTimestamp() &&
hash_equals($characterAuth->token, hash('sha256', $data[1]))
){
// cookie information is valid
// -> try to update character information from CREST
// e.g. Corp has changed, this also ensures valid "access_token"
/**
* @var $character Model\CharacterModel
*/
$character = $characterAuth->characterId;
$updateStatus = $character->updateFromCrest();
// 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. CREST is down,..)
if(
empty($updateStatus) &&
$character->hasUserCharacter() &&
$character->isAuthorized()
){
$characters[$name] = $character;
}
}else{
$invalidCookie = true;
}
$characterAuth->reset();
}else{
$invalidCookie = true;
}
// remove invalid cookie
if($invalidCookie){
$this->getF3()->clear('COOKIE.' . $name);
}
}
}
return $characters;
}
/**
* checks whether a user is currently logged in
* @param \Base $f3
* @return bool
*/
protected function checkLogTimer($f3){
$loginCheck = false;
if($f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) > 0){
// check logIn time
$logInTime = new \DateTime();
$logInTime->setTimestamp( $f3->get(Api\User::SESSION_KEY_CHARACTER_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 model
* @param int $ttl
@@ -127,12 +325,15 @@ class Controller {
$characterId = (int)$this->getF3()->get(Api\User::SESSION_KEY_CHARACTER_ID);
if($characterId){
/**
* @var $characterModel \Model\CharacterModel
* @var $characterModel Model\CharacterModel
*/
$characterModel = Model\BasicModel::getNew('CharacterModel');
$characterModel->getById($characterId, $ttl);
if( !$characterModel->dry() ){
if(
!$characterModel->dry() &&
$characterModel->hasUserCharacter()
){
$character = &$characterModel;
}
}

View File

@@ -40,6 +40,7 @@ class Setup extends Controller {
'Model\UserCharacterModel',
'Model\CharacterModel',
'Model\CharacterAuthenticationModel',
'Model\CharacterLogModel',
'Model\SystemModel',

View File

@@ -375,18 +375,18 @@ abstract class BasicModel extends \DB\Cortex {
/**
* get dataSet by foreign column (single result)
* @param $key
* @param $id
* @param $value
* @param array $options
* @param int $ttl
* @return \DB\Cortex
*/
public function getByForeignKey($key, $id, $options = [], $ttl = 60){
public function getByForeignKey($key, $value, $options = [], $ttl = 60){
$querySet = [];
$query = [];
if($this->exists($key)){
$query[] = $key . " = :" . $key;
$querySet[':' . $key] = $id;
$querySet[':' . $key] = $value;
}
// check active column

View File

@@ -0,0 +1,54 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus
* Date: 25.04.2016
* Time: 19:33
*/
namespace Model;
use DB\SQL\Schema;
class CharacterAuthenticationModel extends BasicModel{
protected $table = 'character_authentication';
protected $fieldConf = [
'active' => [
'type' => Schema::DT_BOOL,
'nullable' => false,
'default' => 1,
'index' => true
],
'characterId' => [
'type' => Schema::DT_INT,
'index' => true,
'belongs-to-one' => 'Model\CharacterModel',
'constraint' => [
[
'table' => 'character',
'on-delete' => 'CASCADE'
]
]
],
'selector' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
'default' => '',
'index' => true,
'unique' => true
],
'token' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
'default' => '',
'index' => true
],
'expires' => [
'type' => Schema::DT_TIMESTAMP,
'default' => Schema::DF_CURRENT_TIMESTAMP,
'index' => true
]
];
}

View File

@@ -9,7 +9,7 @@
namespace Model;
use Controller;
use Controller\Ccp;
use Controller\Ccp\Sso as Sso;
use DB\SQL\Schema;
use Data\Mapper as Mapper;
@@ -93,6 +93,9 @@ class CharacterModel extends BasicModel {
],
'characterMaps' => [
'has-many' => ['Model\CharacterMapModel', 'characterId']
],
'characterTokens' => [
'has-many' => ['Model\CharacterAuthenticationModel', 'characterId']
]
];
@@ -234,7 +237,7 @@ class CharacterModel extends BasicModel {
* get CREST API "access_token" from OAuth
* @return bool|string
*/
private function getAccessToken(){
public function getAccessToken(){
$accessToken = false;
// check if there is already an "accessToken" for this user
@@ -252,7 +255,7 @@ class CharacterModel extends BasicModel {
// add expire time buffer for this "accessToken"
// token should be marked as "deprecated" BEFORE it actually expires.
$timeBuffer = 2 * 60;
$tokenTime->add(new \DateInterval('PT' . (Ccp\Sso::ACCESS_KEY_EXPIRE_TIME - $timeBuffer) . 'S'));
$tokenTime->add(new \DateInterval('PT' . (Sso::ACCESS_KEY_EXPIRE_TIME - $timeBuffer) . 'S'));
$now = new \DateTime('now', $timezone);
if($tokenTime->getTimestamp() > $now->getTimestamp()){
@@ -266,7 +269,7 @@ class CharacterModel extends BasicModel {
!empty($this->crestRefreshToken)
){
// no accessToken found OR token is deprecated
$ssoController = new Ccp\Sso();
$ssoController = new Sso();
$accessData = $ssoController->refreshAccessToken($this->crestRefreshToken);
if(
@@ -363,7 +366,7 @@ class CharacterModel extends BasicModel {
}else{
// get Location Data from CREST endpoint
// user is NOT with IGB online OR has not jet set "trusted" page
$ssoController = new Ccp\Sso();
$ssoController = new Sso();
$logData = $ssoController->getCharacterLocationData($this->getAccessToken(), 10, $additionalOptions);
if($logData['timeout'] === false){
@@ -396,6 +399,64 @@ class CharacterModel extends BasicModel {
return $this;
}
/**
* update character data from CCPs CREST API
* @return array (some status messages)
*/
public function updateFromCrest(){
$status = [];
if( $accessToken = $this->getAccessToken() ){
// et basic character data
// -> this is required for "ownerHash" hash check (e.g. character was sold,..)
// -> the "id" check is just for security and should NEVER fail!
$ssoController = new Sso();
if(
!is_null( $verificationCharacterData = $ssoController->verifyCharacterData($accessToken) ) &&
$verificationCharacterData->CharacterID === $this->_id
){
// get character data from CREST
$characterData = $ssoController->getCharacterData($accessToken);
if( isset($characterData->character) ){
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$corporation = null;
$alliance = null;
if( isset($characterData->corporation) ){
/**
* @var $corporation CorporationModel
*/
$corporation = $this->rel('corporationId');
$corporation->getById($characterData->corporation['id'], 0);
$corporation->copyfrom($characterData->corporation, ['name', 'isNPC']);
$corporation->save();
}
if( isset($characterData->alliance) ){
/**
* @var $alliance AllianceModel
*/
$alliance = $this->rel('allianceId');
$alliance->getById($characterData->alliance['id'], 0);
$alliance->copyfrom($characterData->alliance, ['name']);
$alliance->save();
}
$this->copyfrom($characterData->character, ['name', 'ownerHash']);
$this->set('corporationId', is_object($corporation) ? $corporation->get('id') : null);
$this->set('allianceId', is_object($alliance) ? $corporation->get('id') : null);
$this->save();
}
}else{
$status[] = sprintf(Sso::ERROR_VERIFY_CHARACTER, $this->name);
}
}else{
$status[] = sprintf(Sso::ERROR_ACCESS_TOKEN, $this->name);
}
return $status;
}
/**
* get the character log entry for this character
* @return bool|CharacterLogModel

View File

@@ -35,6 +35,9 @@ INVITE = 0
INVITE_LIMIT = 50
[PATHFINDER.LOGIN]
; expire time (in days) for login cookies
COOKIE_EXPIRE = 30
; restrict login to specific corporations/alliances by id (e.g. 1000166,1000080)
CORPORATION =
ALLIANCE =

View File

@@ -12,12 +12,11 @@ define(['jquery'], function($) {
// user API
getCaptcha: 'api/user/getCaptcha', // ajax URL - get captcha image
sendInviteKey: 'api/user/sendInvite', // ajax URL - send registration key
getCookieCharacterData: 'api/user/getCookieCharacter', // ajax URL - get character data from cookie
logIn: 'api/user/logIn', // ajax URL - login
logOut: 'api/user/logOut', // ajax URL - logout
deleteLog: 'api/user/deleteLog', // ajax URL - delete character log
saveUserConfig: 'api/user/saveAccount', // ajax URL - saves/update user account
getUserData: 'api/user/getData', // ajax URL - get user data
saveSharingConfig: 'api/user/saveSharingConfig', // ajax URL - save "sharing settings" dialog
deleteAccount: 'api/user/deleteAccount', // ajax URL - delete Account data
// access API
searchAccess: 'api/access/search', // ajax URL - search user/corporation/ally by name

View File

@@ -43,11 +43,15 @@ define([
navigationLinkLicenseClass: 'pf-navbar-license', // class for "license" trigger link
navigationVersionLinkClass: 'pf-navbar-version-info', // class for "version information"
// login form
loginFormId: 'pf-login-form', // id for login form
loginButtonClass: 'pf-login-button', // class for "login" button(s)
registerButtonClass: 'pf-register-button', // class for "register" button(s)
loginMessageContainerId: 'pf-login-message-container', // id for login form message container
// cookie hint
cookieHintId: 'pf-cookie-hint', // id for "cookie hint" element
// character select
characterSelectionClass: 'pf-character-selection', // class for character panel wrapper
characterRowAnimateClass: 'pf-character-row-animate', // class for character panel row during animation
characterImageWrapperClass: 'pf-character-image-wrapper', // class for image wrapper (animated)
characterImageInfoClass: 'pf-character-info', // class for character info layer (visible on hover)
dynamicMessageContainerClass: 'pf-dynamic-message-container', // class for "dynamic" (JS) message container
// gallery
galleryId: 'pf-gallery', // id for gallery container
@@ -59,49 +63,75 @@ define([
animateElementClass: 'pf-animate-on-visible' // class for elements that will be animated to show
};
/**
* set a cookie
* @param cname
* @param cvalue
* @param exdays
*/
var setCookie = function(cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + cvalue + '; ' + expires;
};
/**
* get cookie value by name
* @param cname
* @returns {*}
*/
var getCookie = function(cname) {
var name = cname + '=';
var ca = document.cookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length,c.length);
}
}
return '';
};
/**
* set page observer
*/
var setPageObserver = function(){
// login form =====================================================================================
// register buttons ---------------------------------------------
$('.' + config.registerButtonClass).on('click', function(e){
e.preventDefault();
// cookie hint --------------------------------------------------------
if(getCookie('cookie') !== '1'){
// hint not excepted
$('#' + config.cookieHintId).collapse('show');
}
// logout current user (if there e.g. to register a second account)
Util.logout({
ajaxData: {
reroute: 0
}
});
// show register/settings dialog
$.fn.showSettingsDialog({
register: 1,
invite : parseInt( $('body').data('invite') )
});
$('#' + config.cookieHintId + ' .btn-success').on('click', function(){
setCookie('cookie', 1, 365);
});
// releases -----------------------------------------------------
// releases -----------------------------------------------------------
$('.' + config.navigationVersionLinkClass).on('click', function(e){
$.fn.releasesDialog();
});
// manual -------------------------------------------------------
// manual -------------------------------------------------------------
$('.' + config.navigationLinkManualClass).on('click', function(e){
e.preventDefault();
$.fn.showMapManual();
});
// license ------------------------------------------------------
// license ------------------------------------------------------------
$('.' + config.navigationLinkLicenseClass).on('click', function(e){
e.preventDefault();
$.fn.showCreditsDialog(false, true);
});
// tooltips -----------------------------------------------------
// tooltips -----------------------------------------------------------
var mapTooltipOptions = {
toggle: 'tooltip',
container: 'body',
@@ -169,8 +199,6 @@ define([
responseData.error.length > 0
){
form.showFormMessage(responseData.error);
}else{
$('.modal').modal('hide');
Util.showNotify({title: 'Registration Key send', text: 'Check your Mails', type: 'success'});
@@ -240,7 +268,7 @@ define([
return newSlideContent[0];
};
// initialize carousel ------------------------------------------
// initialize carousel ------------------------------------------------
var carousel = new Gallery([
{
title: 'IGB',
@@ -363,29 +391,31 @@ define([
var initYoutube = function(){
$(".youtube").each(function() {
$('.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)');
// Overlay the Play icon to make it look like a video player
$(this).append($('<div/>', {'class': 'play'}));
$(document).delegate('#'+this.id, 'click', function() {
$(document).delegate('#' + this.id, 'click', function() {
// Create an iFrame with autoplay set to true
var iframe_url = "https://www.youtube.com/embed/" + this.id + "?autoplay=1&autohide=1";
if ($(this).data('params')) iframe_url+='&'+$(this).data('params');
var iFrameUrl = 'https://www.youtube.com/embed/' + this.id + '?autoplay=1&autohide=1';
if ( $(this).data('params') ){
iFrameUrl += '&'+$(this).data('params');
}
// The height and width of the iFrame should be the same as parent
var iframe = $('<iframe/>', {
var iFrame = $('<iframe/>', {
frameborder: '0',
src: iframe_url,
src: iFrameUrl,
width: $(this).width(),
height: $(this).height(),
class: 'pricing-big'
});
// Replace the YouTube thumbnail with YouTube HTML5 Player
$(this).replaceWith(iframe);
$(this).replaceWith(iFrame);
});
});
};
@@ -436,6 +466,148 @@ define([
});
};
/**
* load character data from cookie information
* -> all validation is done server side!
*/
var initCharacterSelect = function(){
/**
* init panel animation for an element
* @param imageWrapperElement
*/
var initCharacterAnimation = function(imageWrapperElement){
imageWrapperElement.velocity('stop').delay(300).velocity('transition.flipBounceXIn', {
display: 'inline-block',
stagger: 60,
drag: true,
duration: 600
});
// Hover effect for character info layer
imageWrapperElement.hoverIntent(function(e){
var characterInfoElement = $(this).find('.' + config.characterImageInfoClass);
characterInfoElement.velocity('finish').velocity({
width: ['100%', [ 400, 15 ] ]
},{
easing: 'easeInSine'
});
}, function(e){
var characterInfoElement = $(this).find('.' + config.characterImageInfoClass);
characterInfoElement.velocity('finish').velocity({
width: 0
},{
duration: 150,
easing: 'easeInOutSine'
});
});
};
// --------------------------------------------------------------------
/**
* update all character panels -> set CSS class (e.g. after some panels were added/removed,..)
*/
var updateCharacterPanels = function(){
var characterRows = $('.' + config.characterSelectionClass + ' .pf-dynamic-area').parent();
var rowClassIdentifier = ((12 / characterRows.length ) <= 4) ? 4 : (12 / characterRows.length);
$(characterRows).removeClass().addClass('col-sm-' + rowClassIdentifier);
};
// --------------------------------------------------------------------
var removeCharacterPanel = function(panelElement){
$(panelElement).velocity('transition.expandOut', {
duration: 250,
complete: function () {
// lock row for CSS animations while removing...
$(this).parent().addClass(config.characterRowAnimateClass);
$(this).parent().velocity({
width: 0
},{
easing: 'ease',
duration: 300,
complete: function(){
$(this).remove();
// reset column CSS classes for all existing panels
updateCharacterPanels();
}
});
}
});
};
// --------------------------------------------------------------------
// request character data for each character panel
$('.' + config.characterSelectionClass + ' .pf-dynamic-area').each(function(){
var characterElement = $(this);
characterElement.showLoadingAnimation();
var requestData = {
cookie: characterElement.data('cookie')
};
$.ajax({
type: 'POST',
url: Init.path.getCookieCharacterData,
data: requestData,
dataType: 'json',
context: {
href: characterElement.data('href'),
cookieName: requestData.cookie,
characterElement: characterElement
}
}).done(function(responseData){
var characterElement = this.characterElement;
characterElement.hideLoadingAnimation();
if(
responseData.error &&
responseData.error.length > 0
){
$('.' + config.dynamicMessageContainerClass).showMessage({
type: responseData.error[0].type,
title: 'Character verification failed',
text: responseData.error[0].message
});
}
if(responseData.hasOwnProperty('character')){
var data = {
link: this.href,
cookieName: this.cookieName,
character: responseData.character
};
requirejs(['text!templates/ui/character_panel.html', 'mustache'], function(template, Mustache) {
var content = Mustache.render(template, data);
characterElement.html(content);
// show character panel (animation settings)
initCharacterAnimation(characterElement.find('.' + config.characterImageWrapperClass));
});
}else{
// character data not available -> remove panel
removeCharacterPanel(this.characterElement);
}
}).fail(function( jqXHR, status, error) {
var characterElement = this.characterElement;
characterElement.hideLoadingAnimation();
// character data not available -> remove panel
removeCharacterPanel(this.characterElement);
});
});
};
/**
* main init "landing" page
*/
@@ -490,6 +662,8 @@ define([
// hide splash loading animation
$('.' + config.splashOverlayClass).hideSplashOverlay();
initCharacterSelect();
// init carousel
initCarousel();

File diff suppressed because one or more lines are too long

View File

@@ -12,12 +12,11 @@ define(['jquery'], function($) {
// user API
getCaptcha: 'api/user/getCaptcha', // ajax URL - get captcha image
sendInviteKey: 'api/user/sendInvite', // ajax URL - send registration key
getCookieCharacterData: 'api/user/getCookieCharacter', // ajax URL - get character data from cookie
logIn: 'api/user/logIn', // ajax URL - login
logOut: 'api/user/logOut', // ajax URL - logout
deleteLog: 'api/user/deleteLog', // ajax URL - delete character log
saveUserConfig: 'api/user/saveAccount', // ajax URL - saves/update user account
getUserData: 'api/user/getData', // ajax URL - get user data
saveSharingConfig: 'api/user/saveSharingConfig', // ajax URL - save "sharing settings" dialog
deleteAccount: 'api/user/deleteAccount', // ajax URL - delete Account data
// access API
searchAccess: 'api/access/search', // ajax URL - search user/corporation/ally by name

View File

@@ -43,11 +43,15 @@ define([
navigationLinkLicenseClass: 'pf-navbar-license', // class for "license" trigger link
navigationVersionLinkClass: 'pf-navbar-version-info', // class for "version information"
// login form
loginFormId: 'pf-login-form', // id for login form
loginButtonClass: 'pf-login-button', // class for "login" button(s)
registerButtonClass: 'pf-register-button', // class for "register" button(s)
loginMessageContainerId: 'pf-login-message-container', // id for login form message container
// cookie hint
cookieHintId: 'pf-cookie-hint', // id for "cookie hint" element
// character select
characterSelectionClass: 'pf-character-selection', // class for character panel wrapper
characterRowAnimateClass: 'pf-character-row-animate', // class for character panel row during animation
characterImageWrapperClass: 'pf-character-image-wrapper', // class for image wrapper (animated)
characterImageInfoClass: 'pf-character-info', // class for character info layer (visible on hover)
dynamicMessageContainerClass: 'pf-dynamic-message-container', // class for "dynamic" (JS) message container
// gallery
galleryId: 'pf-gallery', // id for gallery container
@@ -59,49 +63,75 @@ define([
animateElementClass: 'pf-animate-on-visible' // class for elements that will be animated to show
};
/**
* set a cookie
* @param cname
* @param cvalue
* @param exdays
*/
var setCookie = function(cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + cvalue + '; ' + expires;
};
/**
* get cookie value by name
* @param cname
* @returns {*}
*/
var getCookie = function(cname) {
var name = cname + '=';
var ca = document.cookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length,c.length);
}
}
return '';
};
/**
* set page observer
*/
var setPageObserver = function(){
// login form =====================================================================================
// register buttons ---------------------------------------------
$('.' + config.registerButtonClass).on('click', function(e){
e.preventDefault();
// cookie hint --------------------------------------------------------
if(getCookie('cookie') !== '1'){
// hint not excepted
$('#' + config.cookieHintId).collapse('show');
}
// logout current user (if there e.g. to register a second account)
Util.logout({
ajaxData: {
reroute: 0
}
});
// show register/settings dialog
$.fn.showSettingsDialog({
register: 1,
invite : parseInt( $('body').data('invite') )
});
$('#' + config.cookieHintId + ' .btn-success').on('click', function(){
setCookie('cookie', 1, 365);
});
// releases -----------------------------------------------------
// releases -----------------------------------------------------------
$('.' + config.navigationVersionLinkClass).on('click', function(e){
$.fn.releasesDialog();
});
// manual -------------------------------------------------------
// manual -------------------------------------------------------------
$('.' + config.navigationLinkManualClass).on('click', function(e){
e.preventDefault();
$.fn.showMapManual();
});
// license ------------------------------------------------------
// license ------------------------------------------------------------
$('.' + config.navigationLinkLicenseClass).on('click', function(e){
e.preventDefault();
$.fn.showCreditsDialog(false, true);
});
// tooltips -----------------------------------------------------
// tooltips -----------------------------------------------------------
var mapTooltipOptions = {
toggle: 'tooltip',
container: 'body',
@@ -169,8 +199,6 @@ define([
responseData.error.length > 0
){
form.showFormMessage(responseData.error);
}else{
$('.modal').modal('hide');
Util.showNotify({title: 'Registration Key send', text: 'Check your Mails', type: 'success'});
@@ -240,7 +268,7 @@ define([
return newSlideContent[0];
};
// initialize carousel ------------------------------------------
// initialize carousel ------------------------------------------------
var carousel = new Gallery([
{
title: 'IGB',
@@ -363,29 +391,31 @@ define([
var initYoutube = function(){
$(".youtube").each(function() {
$('.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)');
// Overlay the Play icon to make it look like a video player
$(this).append($('<div/>', {'class': 'play'}));
$(document).delegate('#'+this.id, 'click', function() {
$(document).delegate('#' + this.id, 'click', function() {
// Create an iFrame with autoplay set to true
var iframe_url = "https://www.youtube.com/embed/" + this.id + "?autoplay=1&autohide=1";
if ($(this).data('params')) iframe_url+='&'+$(this).data('params');
var iFrameUrl = 'https://www.youtube.com/embed/' + this.id + '?autoplay=1&autohide=1';
if ( $(this).data('params') ){
iFrameUrl += '&'+$(this).data('params');
}
// The height and width of the iFrame should be the same as parent
var iframe = $('<iframe/>', {
var iFrame = $('<iframe/>', {
frameborder: '0',
src: iframe_url,
src: iFrameUrl,
width: $(this).width(),
height: $(this).height(),
class: 'pricing-big'
});
// Replace the YouTube thumbnail with YouTube HTML5 Player
$(this).replaceWith(iframe);
$(this).replaceWith(iFrame);
});
});
};
@@ -436,6 +466,148 @@ define([
});
};
/**
* load character data from cookie information
* -> all validation is done server side!
*/
var initCharacterSelect = function(){
/**
* init panel animation for an element
* @param imageWrapperElement
*/
var initCharacterAnimation = function(imageWrapperElement){
imageWrapperElement.velocity('stop').delay(300).velocity('transition.flipBounceXIn', {
display: 'inline-block',
stagger: 60,
drag: true,
duration: 600
});
// Hover effect for character info layer
imageWrapperElement.hoverIntent(function(e){
var characterInfoElement = $(this).find('.' + config.characterImageInfoClass);
characterInfoElement.velocity('finish').velocity({
width: ['100%', [ 400, 15 ] ]
},{
easing: 'easeInSine'
});
}, function(e){
var characterInfoElement = $(this).find('.' + config.characterImageInfoClass);
characterInfoElement.velocity('finish').velocity({
width: 0
},{
duration: 150,
easing: 'easeInOutSine'
});
});
};
// --------------------------------------------------------------------
/**
* update all character panels -> set CSS class (e.g. after some panels were added/removed,..)
*/
var updateCharacterPanels = function(){
var characterRows = $('.' + config.characterSelectionClass + ' .pf-dynamic-area').parent();
var rowClassIdentifier = ((12 / characterRows.length ) <= 4) ? 4 : (12 / characterRows.length);
$(characterRows).removeClass().addClass('col-sm-' + rowClassIdentifier);
};
// --------------------------------------------------------------------
var removeCharacterPanel = function(panelElement){
$(panelElement).velocity('transition.expandOut', {
duration: 250,
complete: function () {
// lock row for CSS animations while removing...
$(this).parent().addClass(config.characterRowAnimateClass);
$(this).parent().velocity({
width: 0
},{
easing: 'ease',
duration: 300,
complete: function(){
$(this).remove();
// reset column CSS classes for all existing panels
updateCharacterPanels();
}
});
}
});
};
// --------------------------------------------------------------------
// request character data for each character panel
$('.' + config.characterSelectionClass + ' .pf-dynamic-area').each(function(){
var characterElement = $(this);
characterElement.showLoadingAnimation();
var requestData = {
cookie: characterElement.data('cookie')
};
$.ajax({
type: 'POST',
url: Init.path.getCookieCharacterData,
data: requestData,
dataType: 'json',
context: {
href: characterElement.data('href'),
cookieName: requestData.cookie,
characterElement: characterElement
}
}).done(function(responseData){
var characterElement = this.characterElement;
characterElement.hideLoadingAnimation();
if(
responseData.error &&
responseData.error.length > 0
){
$('.' + config.dynamicMessageContainerClass).showMessage({
type: responseData.error[0].type,
title: 'Character verification failed',
text: responseData.error[0].message
});
}
if(responseData.hasOwnProperty('character')){
var data = {
link: this.href,
cookieName: this.cookieName,
character: responseData.character
};
requirejs(['text!templates/ui/character_panel.html', 'mustache'], function(template, Mustache) {
var content = Mustache.render(template, data);
characterElement.html(content);
// show character panel (animation settings)
initCharacterAnimation(characterElement.find('.' + config.characterImageWrapperClass));
});
}else{
// character data not available -> remove panel
removeCharacterPanel(this.characterElement);
}
}).fail(function( jqXHR, status, error) {
var characterElement = this.characterElement;
characterElement.hideLoadingAnimation();
// character data not available -> remove panel
removeCharacterPanel(this.characterElement);
});
});
};
/**
* main init "landing" page
*/
@@ -490,6 +662,8 @@ define([
// hide splash loading animation
$('.' + config.splashOverlayClass).hideSplashOverlay();
initCharacterSelect();
// init carousel
initCarousel();

View File

@@ -0,0 +1,23 @@
<a href="{{ link }}?cookie={{ cookieName }}">
<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 }}"/>
<div class="pf-character-info">
<div style="width: 128px">
{{#character.corporation}}
<img src="https://image.eveonline.com/Corporation/{{ id }}_32.png"/>
<div class="pf-character-info-text"><small>{{ name }}</small></div>
{{/character.corporation}}
{{#character.alliance}}
<img src="https://image.eveonline.com/Alliance/{{ id }}_32.png"/>
<div class="pf-character-info-text"><small>{{ name }}</small></div>
{{/character.alliance}}
</div>
</div>
</div>
<div class="pf-character-name">
<small>{{ character.name }}</small>
</div>
</div>
</a>

View File

@@ -70,66 +70,96 @@
{* login form *}
<section id="pf-landing-login">
<div class="container">
<div class="row text-center">
<div class="col-md-8 col-md-offset-2">
<h2><span class="text-primary">Please</span> log in</h2>
</div>
</div>
<form id="pf-login-form" class="" role="form" method="post" action="#">
{* login message container *}
<check if="{{ @SESSION.SSO.ERROR }}">
{* cookie characters *}
<check if="{{ @cookieCharacters }}">
<true>
<div class="row text-center">
<div class="col-md-8 col-md-offset-2">
<h2><span class="text-primary">Select</span> your character</h2>
</div>
</div>
{* dynamic message container *}
<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 class="col-sm-8 col-sm-offset-2 pf-dynamic-message-container">
{* client side message e.g. ajax errors *}
</div>
</div>
</div>
</check>
{* check for setup mode *}
<check if="{{ array_key_exists('/setup', @ROUTES) && @PATHFINDER.SHOW_SETUP_WARNING }}">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<div class="alert alert-warning" >
<span class="txt-color txt-color-warning">Setup mode active</span>
<small>Disable setup route in routes.ini in production environment! <a href="/setup">Setup</a></small>
{* 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>
</div>
</check>
</check>
{* IGB warning *}
<check if="{{ @isIngame == 1 }}">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<div class="alert alert-warning" >
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><i class="fa fa-close"></i></button>
<span class="txt-color txt-color-warning">Hint</span>
<small>Some functions may not work within the outdated IGB browser!</small>
{* check for setup mode *}
<check if="{{ array_key_exists('/setup', @ROUTES) && @PATHFINDER.SHOW_SETUP_WARNING }}">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<div class="alert alert-warning" >
<span class="txt-color txt-color-warning">Setup mode active</span>
<small>Disable setup route in routes.ini in production environment! <a href="/setup">Setup</a></small>
</div>
</div>
</div>
</div>
</div>
</check>
</check>
{* SSO login *}
<div class="container-fluid">
{* IGB warning *}
<check if="{{ @isIngame == 1 }}">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<div class="alert alert-warning" >
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><i class="fa fa-close"></i></button>
<span class="txt-color txt-color-warning">Hint</span>
<small>Some functions may not work within the outdated IGB browser!</small>
</div>
</div>
</div>
</div>
</check>
<div class="row text-center pf-character-selection">
<repeat group="{{ @cookieCharacters }}" key="{{ @cookieName }}" value="{{ @characterData }}">
<div class="col-sm-{{ @getCharacterGrid( @cookieCharacters ) }}">
<div class="pf-dynamic-area" data-cookie="{{ @cookieName }}" data-href="{{ 'sso','action=login' | alias }}">
{* character data is dynamically *}
</div>
</div>
</repeat>
</div>
</true>
<false>
<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 class="col-md-8 col-md-offset-2">
<h2><span class="text-primary">Sign</span> up</h2>
</div>
</div>
</false>
</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>
</form>
</div>
</div>
</section>
@@ -893,6 +923,17 @@
</div>
</footer>
{* cookie warning *}
<footer class="navbar-default navbar-fixed-bottom collapse" id="pf-cookie-hint">
<div class="container-fluid">
<p class="navbar-text">
Pathfinder requires cookies to maintain your login state between browser sessions. Read <a target="_blank" href="https://github.com/exodus4d/pathfinder/issues/138">more</a>.
</p>
<div class="pull-right">
<button type="button" class="btn btn-default navbar-btn btn-success" data-toggle="collapse" data-target="#pf-cookie-hint" aria-expanded="false">That´s fine</button>
</div>
</div>
</footer>
{* hidden dialog *}
<include href="templates/dialog/gallery.html" />

View File

@@ -320,6 +320,87 @@
margin-bottom: 0;
}
// character select panel animation (e.g. on delete)
.pf-character-selection > div:not(.pf-character-row-animate){
@include transition( width 0.2s ease, margin 0.2s ease);
}
.pf-dynamic-area{
display: inline-block;
margin: 10px 5px 20px 5px;
padding: 10px 10px 5px 10px;
min-width: 155px;
min-height: 184px;
@include border-radius(10px);
@include box-shadow(0 4px 10px rgba(0,0,0, 0.4));
// character images
.pf-character-image-wrapper{
opacity: 0;
width: 128px;
border: 2px solid $gray-light;
@include border-radius(8px);
@include transition( border-color 0.2s ease-out, box-shadow 0.2s ease-out);
@include transform( translate3d(0, 0, 0) );
will-change: border-color, transition;
overflow: hidden;
cursor: pointer;
display: inline-block;
background-color: $gray-darker;
box-sizing: content-box; // because of the borders and the fix img with of 128
&:hover{
border-color: $green-dark;
.pf-character-name{
color: $green-dark;
}
.pf-character-image{
@include filter(grayscale(50%))
}
}
.pf-character-select-image{
overflow: hidden;
width: 128px;
height: 128px;
position: relative;
// info element visible on hover
.pf-character-info{
position: absolute;
top: 0;
left: 0;
width: 0; // trigger by js
height: 100%;
color: $gray-lighter;
background: rgba($gray, 0.80);
overflow: hidden;
will-change: width, transition;
padding: 10px 0;
.pf-character-info-text{
line-height: 25px;
}
}
}
.pf-character-name{
font-size: 13px;
line-height: 30px;
border-top: 1px solid $gray-dark;
color: $gray-lighter;
@include transition( color 0.2s ease-out );
}
.pf-character-image{
@include transition(all 0.3s ease-out);
@include filter(grayscale(0%));
}
}
}
.pf-sso-login-button{
position: relative;
display: inline-block;

View File

@@ -989,3 +989,4 @@ Animate the stripes