Merge branch 'master' into Add-email-sender-class

# Conflicts:
#	server/composer.json
#	server/controllers/user/signup.php
This commit is contained in:
ivan 2016-07-14 01:45:50 -03:00
commit 948b476c12
29 changed files with 439 additions and 103 deletions

View File

@ -1,13 +1,23 @@
import React from 'react'; const React = require('react');
import {Router, Route, IndexRoute, browserHistory} from 'react-router'; const {Router, Route, IndexRoute, browserHistory} = require('react-router');
import App from 'app/App'; const App = require('app/App');
import DemoPage from 'app/demo/components-demo-page'; const DemoPage = require('app/demo/components-demo-page');
import MainLayout from 'app/main/main-layout'; const MainLayout = require('app/main/main-layout');
import MainHomePage from 'app/main/main-home/main-home-page'; const MainHomePage = require('app/main/main-home/main-home-page');
import MainSignUpPage from 'app/main/main-signup/main-signup-page'; const MainSignUpPage = require('app/main/main-signup/main-signup-page');
const DashboardLayout = require('app/main/dashboard/dashboard-layout');
const DashboardListTicketsPage = require('app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page');
const DashboardListArticlesPage = require('app/main/dashboard/dashboard-list-articles/dashboard-list-articles-page');
const DashboardCreateTicketPage = require('app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page');
const DashboardEditProfilePage = require('app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page');
const DashboardArticlePage = require('app/main/dashboard/dashboard-article/dashboard-article-page');
const DashboardTicketPage = require('app/main/dashboard/dashboard-ticket/dashboard-ticket-page');
export default ( export default (
<Router history={browserHistory}> <Router history={browserHistory}>
@ -15,6 +25,16 @@ export default (
<Route path='/app' component={MainLayout}> <Route path='/app' component={MainLayout}>
<IndexRoute component={MainHomePage} /> <IndexRoute component={MainHomePage} />
<Route path='signup' component={MainSignUpPage}/> <Route path='signup' component={MainSignUpPage}/>
<Route path='dashboard' component={DashboardLayout}>
<IndexRoute component={DashboardListTicketsPage} />
<Route path='articles' component={DashboardListArticlesPage}/>
<Route path='create-ticket' component={DashboardCreateTicketPage}/>
<Route path='edit-profile' component={DashboardEditProfilePage}/>
<Route path='article' component={DashboardArticlePage}/>
<Route path='ticket' component={DashboardTicketPage}/>
</Route>
</Route> </Route>
<Route name='Demo' path='demo' component={DemoPage} /> <Route name='Demo' path='demo' component={DemoPage} />

View File

@ -0,0 +1,14 @@
import React from 'react';
const DashboardArticlePage = React.createClass({
render() {
return (
<div>
DASHBOARD ARTICLE
</div>
);
}
});
export default DashboardArticlePage;

View File

@ -0,0 +1,14 @@
import React from 'react';
const DashboardCreateTicketPage = React.createClass({
render() {
return (
<div>
DASHBOARD CREATE TICKET
</div>
);
}
});
export default DashboardCreateTicketPage;

View File

@ -0,0 +1,14 @@
import React from 'react';
const DashboardEditProfilePage = React.createClass({
render() {
return (
<div>
DASHBOARD EDIT PROFILE
</div>
);
}
});
export default DashboardEditProfilePage;

View File

@ -0,0 +1,16 @@
import React from 'react';
import DashboardMenu from 'app/main/dashboard/dashboard-menu';
const DashboardLayout = React.createClass({
render() {
return (
<div>
<div><DashboardMenu location={this.props.location} /></div>
<div>{this.props.children}</div>
</div>
);
}
});
export default DashboardLayout;

View File

@ -0,0 +1,14 @@
import React from 'react';
const DashboardListArticlesPage = React.createClass({
render() {
return (
<div>
DASHBOARD ARTICLES LIST
</div>
);
}
});
export default DashboardListArticlesPage;

View File

@ -0,0 +1,14 @@
import React from 'react';
const DashboardListTicketsPage = React.createClass({
render() {
return (
<div>
DASHBOARD TICKET LIST
</div>
);
}
});
export default DashboardListTicketsPage;

View File

@ -0,0 +1,57 @@
import React from 'react';
import _ from 'lodash';
import Menu from 'core-components/menu';
let dashboardRoutes = [
{ path: '/app/dashboard', text: 'Ticket List' },
{ path: '/app/dashboard/create-ticket', text: 'Create Ticket' },
{ path: '/app/dashboard/articles', text: 'View Articles' },
{ path: '/app/dashboard/edit-profile', text: 'Edit Profile' }
];
const DashboardMenu = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
location: React.PropTypes.object
},
render() {
return (
<Menu {...this.getProps()} />
);
},
getProps() {
return {
items: this.getMenuItems(),
selectedIndex: this.getSelectedIndex(),
onItemClick: this.goToPathByIndex
};
},
getMenuItems: function () {
return dashboardRoutes.map(this.getMenuItem);
},
getMenuItem(item) {
return {
content: item.text
};
},
getSelectedIndex() {
let pathname = this.props.location.pathname;
return _.findIndex(dashboardRoutes, {path: pathname});
},
goToPathByIndex(itemIndex) {
this.context.router.push(dashboardRoutes[itemIndex].path);
}
});
export default DashboardMenu;

View File

@ -0,0 +1,14 @@
import React from 'react';
const DashboardTicketPage = React.createClass({
render() {
return (
<div>
DASHBOARD TICKET PAGE
</div>
);
}
});
export default DashboardTicketPage;

View File

@ -26,7 +26,7 @@ const Icon = React.createClass({
renderFlag() { renderFlag() {
return ( return (
<img className={this.props.className} src={`../images/icons/${this.props.name}.png`} aria-hidden="true" /> <img className={this.props.className} src={`/images/icons/${this.props.name}.png`} aria-hidden="true" />
); );
}, },

View File

@ -11,6 +11,7 @@
&__list-item { &__list-item {
padding: 8px; padding: 8px;
&_selected,
&:hover { &:hover {
background-color: $primary-red; background-color: $primary-red;
color: white; color: white;
@ -23,6 +24,7 @@
} }
&_secondary { &_secondary {
.menu__list-item_selected,
.menu__list-item:hover { .menu__list-item:hover {
background-color: $secondary-blue; background-color: $secondary-blue;
} }

View File

@ -2,6 +2,7 @@
"require": { "require": {
"slim/slim": "~2.0", "slim/slim": "~2.0",
"gabordemooij/redbean": "~4.2", "gabordemooij/redbean": "~4.2",
"respect/validation": "^1.1",
"phpmailer/phpmailer": "^5.2" "phpmailer/phpmailer": "^5.2"
}, },
"require-dev": { "require-dev": {

View File

@ -1,9 +1,11 @@
<?php <?php
include 'ticket/create.php'; include 'ticket/create.php';
include 'ticket/comment.php';
$ticketControllers = new ControllerGroup(); $ticketControllers = new ControllerGroup();
$ticketControllers->setGroupPath('/ticket'); $ticketControllers->setGroupPath('/ticket');
$ticketControllers->addController(new CreateController); $ticketControllers->addController(new CreateController);
$ticketControllers->addController(new CommentController);
$ticketControllers->finalize(); $ticketControllers->finalize();

View File

@ -0,0 +1,39 @@
<?php
use RedBeanPHP\Facade as RedBean;
class CommentController extends Controller {
const PATH = '/comment';
private $ticketId;
private $content;
public function validations() {
return [
'permission' => 'any',
'requestData' => []
];
}
public function handler() {
$this->requestData();
$this->storeComment();
Response::respondSuccess();
}
private function requestData() {
$this->ticketId = Controller::request('ticketId');
$this->content = Controller::request('content');
}
private function storeComment() {
$comment = new Comment();
$comment->setProperties(array(
'content' => $this->content
));
$ticket = Ticket::getTicket($this->ticketId);
$ticket->addComment($comment);
$ticket->store();
}
}

View File

@ -1,51 +1,46 @@
<?php <?php
use RedBeanPHP\Facade as RedBean;
use Respect\Validation\Validator as DataValidator;
class CreateController extends Controller { class CreateController extends Controller {
const PATH = '/create'; const PATH = '/create';
private $title ; private $title;
private $content; private $content;
private $departmentId; private $departmentId;
private $language; private $language;
public function handler() { public function validations() {
$this->requestTicketData(); return [
'permission' => 'any',
$validateResult = $this->validateData(); 'requestData' => [
'title' => [
if ($validateResult !== true) { 'validation' => DataValidator::length(3, 30),
Response::respondError($validateResult); 'error' => ERRORS::INVALID_TITLE
} else { ],
$this->storeTicket(); 'content' => [
'validation' => DataValidator::length(10, 500),
Response::respondSuccess(); 'error' => ERRORS::INVALID_CONTENT
} ]
]
];
} }
private function requestTicketData() { public function handler() {
$this->storeRequestData();
$this->storeTicket();
Response::respondSuccess();
}
private function storeRequestData() {
$this->title = Controller::request('title'); $this->title = Controller::request('title');
$this->content = Controller::request('content'); $this->content = Controller::request('content');
$this->departmentId = Controller::request('departmentId'); $this->departmentId = Controller::request('departmentId');
$this->language = Controller::request('language'); $this->language = Controller::request('language');
} }
private function validateData() {
if (strlen($this->title) < 3) {
return ERRORS::SHORT_TITLE;
}
if (strlen($this->title) > 30) {
return ERRORS::LONG_TITLE;
}
if (strlen($this->content) < 5) {
return ERRORS::SHORT_CONTENT;
}
if (strlen($this->content) > 500) {
return ERRORS::LONG_CONTENT;
}
return true;
}
private function storeTicket() { private function storeTicket() {
$ticket = new Ticket(); $ticket = new Ticket();
$ticket->setProperties(array( $ticket->setProperties(array(
@ -55,13 +50,14 @@ class CreateController extends Controller {
'language' => $this->language, 'language' => $this->language,
'department' => $this->departmentId, 'department' => $this->departmentId,
'file' => '', 'file' => '',
'date' => date("F j, Y, g:i a"), 'date' => date('F j, Y, g:i a'),
'unread' => false, 'unread' => false,
'closed' => false, 'closed' => false
'author' => '',
'owner'=> '',
'ownComments' => []
)); ));
//TODO: Add logged user as author
$ticket->setAuthor(User::getUser(1));
$ticket->store(); $ticket->store();
} }
} }

View File

@ -5,6 +5,13 @@ class LoginController extends Controller {
private $userInstance; private $userInstance;
private $session; private $session;
public function validations() {
return [
'permission' => 'any',
'requestData' => []
];
}
public function handler() { public function handler() {
if ($this->isAlreadyLoggedIn()) { if ($this->isAlreadyLoggedIn()) {

View File

@ -2,6 +2,13 @@
class LogoutController extends Controller { class LogoutController extends Controller {
const PATH = '/logout'; const PATH = '/logout';
public function validations() {
return [
'permission' => 'any',
'requestData' => []
];
}
public function handler() { public function handler() {
$session = Session::getInstance(); $session = Session::getInstance();
$session->closeSession(); $session->closeSession();

View File

@ -3,24 +3,25 @@
class SignUpController extends Controller { class SignUpController extends Controller {
const PATH = '/signup'; const PATH = '/signup';
private $email; public function validations() {
private $password; return [
'permission' => 'any',
'requestData' => []
];
}
public function handler() { public function handler() {
$this->requestUserData(); $email = Controller::request('email');
$password = Controller::request('password');
$userId = $this->createNewUserAndRetrieveId($this->email, $this->password); $userId = $this->createNewUserAndRetrieveId($email, $password);
EmailSender::validRegister($email);
Response::respondSuccess(array( Response::respondSuccess(array(
'userId' => $userId, 'userId' => $userId,
'userEmail' => $this->email 'userEmail' => $email
)); ));
EmailSender::validRegister($this->email);
}
public function requestUserData(){
$this->email = Controller::request('email');
$this->password = Controller::request('password');
} }
public function createNewUserAndRetrieveId($email, $password) { public function createNewUserAndRetrieveId($email, $password) {

View File

@ -21,13 +21,13 @@ spl_autoload_register(function ($class) {
$classPath = "models/{$class}.php"; $classPath = "models/{$class}.php";
if(file_exists($classPath)) { if(file_exists($classPath)) {
include $classPath; include_once $classPath;
} }
}); });
// LOAD CONTROLLERS // LOAD CONTROLLERS
foreach (glob('controllers/*.php') as $controller) { foreach (glob('controllers/*.php') as $controller) {
include $controller; include_once $controller;
} }
$app->run(); $app->run();

View File

@ -1,4 +1,5 @@
<?php <?php
require_once 'libs/Validator.php';
abstract class Controller { abstract class Controller {
@ -6,40 +7,37 @@ abstract class Controller {
* Instance-related stuff * Instance-related stuff
*/ */
abstract public function handler(); abstract public function handler();
abstract public function validations();
public function getHandler() { public function getHandler() {
return function () { return function () {
try {
$this->validate();
} catch (ValidationException $exception) {
Response::respondError($exception->getMessage());
return;
}
$this->handler(); $this->handler();
}; };
} }
public function validate() {
$validator = new Validator();
$validator->validate($this->validations());
}
public static function request($key) { public static function request($key) {
$app = self::getAppInstance(); $app = self::getAppInstance();
return $app->request()->post($key); return $app->request()->post($key);
} }
public static function checkUserLogged() {
$session = Session::getInstance();
return $session->checkAuthentication(array(
'user_id' => self::request('csrf_userid'),
'token' => self::request('csrf_token')
));
}
public static function getLoggedUser() { public static function getLoggedUser() {
return User::getUser((int)self::request('csrf_userid')); return User::getUser((int)self::request('csrf_userid'));
} }
public static function checkStaffLogged() {
return self::checkUserLogged() && (self::getLoggedUser()->admin === 1);
}
public static function checkAdminLogged() {
return self::checkUserLogged() && (self::getLoggedUser()->admin === 2);
}
public static function getAppInstance() { public static function getAppInstance() {
return \Slim\Slim::getInstance(); return \Slim\Slim::getInstance();
} }

61
server/libs/Validator.php Normal file
View File

@ -0,0 +1,61 @@
<?php
require_once 'libs/Controller.php';
use Respect\Validation\Validator as DataValidator;
class ValidationException extends Exception {}
class Validator {
public function validate($config) {
$this->validatePermissions($config['permission']);
$this->validateAllRequestData($config['requestData']);
}
private function validatePermissions($permission) {
$permissions = [
'any' => true,
'user' => $this->isUserLogged(),
'staff' => $this->isStaffLogged(),
'admin' => $this->isAdminLogged()
];
if (!$permissions[$permission]) {
throw new ValidationException(ERRORS::NO_PERMISSION);
}
}
private function validateAllRequestData($requestDataValidations) {
foreach ($requestDataValidations as $requestDataKey => $requestDataValidationConfig) {
$requestDataValue = Controller::request($requestDataKey);
$requestDataValidator = $requestDataValidationConfig['validation'];
$requestDataValidationErrorMessage = $requestDataValidationConfig['error'];
$this->validateData($requestDataValue, $requestDataValidator, $requestDataValidationErrorMessage);
}
}
private function validateData($value, DataValidator $dataValidator, $error) {
if (!$dataValidator->validate($value)) {
throw new ValidationException($error);
}
}
private function isUserLogged() {
$session = Session::getInstance();
return $session->checkAuthentication(array(
'userId' => Controller::request('csrf_userid'),
'token' => Controller::request('csrf_token')
));
}
private function isStaffLogged() {
return $this->isUserLogged() && (Controller::getLoggedUser()->admin === 1);
}
private function isAdminLogged() {
return $this->isUserLogged() && (Controller::getLoggedUser()->admin === 2);
}
}

View File

@ -1,7 +1,7 @@
<?php <?php
class Comment extends DataStore { class Comment extends DataStore {
const TABLE = 'comments'; const TABLE = 'comment';
public static function getProps() { public static function getProps() {
return array( return array(

View File

@ -2,8 +2,7 @@
class ERRORS { class ERRORS {
const INVALID_CREDENTIALS = 'User or password is not defined'; const INVALID_CREDENTIALS = 'User or password is not defined';
const SESSION_EXISTS = 'User is already logged in'; const SESSION_EXISTS = 'User is already logged in';
const SHORT_TITLE = 'Title is too short'; const NO_PERMISSION = 'You have no permission to access';
const LONG_TITLE = 'Title is very long'; const INVALID_TITLE = 'Invalid title';
const SHORT_CONTENT = 'Content is too short'; const INVALID_CONTENT = 'Invalid content';
const LONG_CONTENT = 'Content is very long';
} }

View File

@ -38,8 +38,12 @@ class Session {
} }
public function checkAuthentication($data) { public function checkAuthentication($data) {
return $this->getStoredData('userId') === $data['userId'] && $userId = $this->getStoredData('userId');
$this->getStoredData('token') === $data['token']; $token = $this->getStoredData('token');
return $userId && $token &&
$userId === $data['userId'] &&
$token === $data['token'];
} }
public function isLoggedWithId($userId) { public function isLoggedWithId($userId) {

View File

@ -1,11 +1,14 @@
<?php <?php
use RedBeanPHP\Facade as RedBean;
class Ticket extends DataStore { class Ticket extends DataStore {
const TABLE = 'tickets'; const TABLE = 'ticket';
private $author;
public static function getProps() { public static function getProps() {
return array( return array(
'ticketId', 'ticketNumber',
'title', 'title',
'content', 'content',
'language', 'language',
@ -16,11 +19,38 @@ class Ticket extends DataStore {
'closed', 'closed',
'author', 'author',
'owner', 'owner',
'ownComments' 'ownCommentList'
); );
} }
protected function getDefaultProps() { public static function getTicket($value, $property = 'id') {
return array(); return parent::getDataStore($value, $property);
}
public function getDefaultProps() {
return array(
'owner' => null
);
}
public function setAuthor(User $user) {
$this->author = $user;
$this->author->addTicket($this);
$this->setProperties(array(
'author' => $this->author->getBeanInstance()
));
}
public function addComment(Comment $comment) {
$this->getBeanInstance()->ownCommentList[] = $comment->getBeanInstance();
}
public function store() {
parent::store();
if ($this->author instanceof User) {
$this->author->store();
}
} }
} }

View File

@ -1,7 +1,8 @@
<?php <?php
use RedBeanPHP\Facade as RedBean;
class User extends DataStore { class User extends DataStore {
const TABLE = 'users'; const TABLE = 'user';
public static function authenticate($userEmail, $userPassword) { public static function authenticate($userEmail, $userPassword) {
$user = User::getUser($userEmail, 'email'); $user = User::getUser($userEmail, 'email');
@ -14,19 +15,16 @@ class User extends DataStore {
'email', 'email',
'password', 'password',
'name', 'name',
'verificationToken', 'verificationToken'
'ownTickets'
); );
} }
public function getDefaultProps() { public function getDefaultProps() {
return array( return array();
'ownTickets' => []
);
} }
public function addTicket($ticket) { public function addTicket(Ticket $ticket) {
$this->ownTickets[] = $ticket; $this->getBeanInstance()->sharedTicketList[] = $ticket->getBeanInstance();
} }
public static function getUser($value, $property = 'id') { public static function getUser($value, $property = 'id') {

14
tests/ticket/comment.rb Normal file
View File

@ -0,0 +1,14 @@
describe 'ticket/comment/' do
it 'should fail if not logged' do
end
describe 'on successful request' do
it 'should add comment to current ticket' do
end
it 'should link the comment to author' do
end
end
end

View File

@ -1,11 +1,11 @@
describe '/user/login' do describe '/ticket/create' do
it 'should fail if title is too short' do it 'should fail if title is too short' do
result = request('/ticket/create',{ result = request('/ticket/create', {
title: 'GG' title: 'GG'
}) })
(result['status']).should.equal('fail') (result['status']).should.equal('fail')
(result['message']).should.equal('Title is too short') (result['message']).should.equal('Invalid title')
end end
@ -15,7 +15,7 @@ describe '/user/login' do
}) })
(result['status']).should.equal('fail') (result['status']).should.equal('fail')
(result['message']).should.equal('Title is very long') (result['message']).should.equal('Invalid title')
end end
@ -26,7 +26,7 @@ describe '/user/login' do
}) })
(result['status']).should.equal('fail') (result['status']).should.equal('fail')
(result['message']).should.equal('Content is too short') (result['message']).should.equal('Invalid content')
end end
it 'should fail if content is very long' do it 'should fail if content is very long' do
@ -39,7 +39,7 @@ describe '/user/login' do
}) })
(result['status']).should.equal('fail') (result['status']).should.equal('fail')
(result['message']).should.equal('Content is very long') (result['message']).should.equal('Invalid content')
end end
@ -50,7 +50,7 @@ describe '/user/login' do
}) })
(result['status']).should.equal('success') (result['status']).should.equal('success')
ticket = $database.getRow('tickets','Winter is coming','title') ticket = $database.getRow('ticket','Winter is coming','title')
(ticket['content']).should.equal('The north remembers') (ticket['content']).should.equal('The north remembers')
end end
end end

View File

@ -5,7 +5,7 @@ describe '/user/signup' do
'password' => 'custom' 'password' => 'custom'
}) })
userRow = $database.getRow('users', response['data']['userId']) userRow = $database.getRow('user', response['data']['userId'])
(userRow['email']).should.equal('steve@jobs.com') (userRow['email']).should.equal('steve@jobs.com')
end end