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';
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
const React = require('react');
const {Router, Route, IndexRoute, browserHistory} = require('react-router');
import App from 'app/App';
import DemoPage from 'app/demo/components-demo-page';
const App = require('app/App');
const DemoPage = require('app/demo/components-demo-page');
import MainLayout from 'app/main/main-layout';
import MainHomePage from 'app/main/main-home/main-home-page';
import MainSignUpPage from 'app/main/main-signup/main-signup-page';
const MainLayout = require('app/main/main-layout');
const MainHomePage = require('app/main/main-home/main-home-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 (
<Router history={browserHistory}>
@ -15,6 +25,16 @@ export default (
<Route path='/app' component={MainLayout}>
<IndexRoute component={MainHomePage} />
<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 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() {
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 {
padding: 8px;
&_selected,
&:hover {
background-color: $primary-red;
color: white;
@ -23,6 +24,7 @@
}
&_secondary {
.menu__list-item_selected,
.menu__list-item:hover {
background-color: $secondary-blue;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,24 +3,25 @@
class SignUpController extends Controller {
const PATH = '/signup';
private $email;
private $password;
public function validations() {
return [
'permission' => 'any',
'requestData' => []
];
}
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(
'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) {

View File

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

View File

@ -1,4 +1,5 @@
<?php
require_once 'libs/Validator.php';
abstract class Controller {
@ -6,40 +7,37 @@ abstract class Controller {
* Instance-related stuff
*/
abstract public function handler();
abstract public function validations();
public function getHandler() {
return function () {
try {
$this->validate();
} catch (ValidationException $exception) {
Response::respondError($exception->getMessage());
return;
}
$this->handler();
};
}
public function validate() {
$validator = new Validator();
$validator->validate($this->validations());
}
public static function request($key) {
$app = self::getAppInstance();
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() {
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() {
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
class Comment extends DataStore {
const TABLE = 'comments';
const TABLE = 'comment';
public static function getProps() {
return array(

View File

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

View File

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

View File

@ -1,11 +1,14 @@
<?php
use RedBeanPHP\Facade as RedBean;
class Ticket extends DataStore {
const TABLE = 'tickets';
const TABLE = 'ticket';
private $author;
public static function getProps() {
return array(
'ticketId',
'ticketNumber',
'title',
'content',
'language',
@ -16,11 +19,38 @@ class Ticket extends DataStore {
'closed',
'author',
'owner',
'ownComments'
'ownCommentList'
);
}
protected function getDefaultProps() {
return array();
public static function getTicket($value, $property = 'id') {
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
use RedBeanPHP\Facade as RedBean;
class User extends DataStore {
const TABLE = 'users';
const TABLE = 'user';
public static function authenticate($userEmail, $userPassword) {
$user = User::getUser($userEmail, 'email');
@ -14,19 +15,16 @@ class User extends DataStore {
'email',
'password',
'name',
'verificationToken',
'ownTickets'
'verificationToken'
);
}
public function getDefaultProps() {
return array(
'ownTickets' => []
);
return array();
}
public function addTicket($ticket) {
$this->ownTickets[] = $ticket;
public function addTicket(Ticket $ticket) {
$this->getBeanInstance()->sharedTicketList[] = $ticket->getBeanInstance();
}
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
result = request('/ticket/create',{
result = request('/ticket/create', {
title: 'GG'
})
(result['status']).should.equal('fail')
(result['message']).should.equal('Title is too short')
(result['message']).should.equal('Invalid title')
end
@ -15,7 +15,7 @@ describe '/user/login' do
})
(result['status']).should.equal('fail')
(result['message']).should.equal('Title is very long')
(result['message']).should.equal('Invalid title')
end
@ -26,7 +26,7 @@ describe '/user/login' do
})
(result['status']).should.equal('fail')
(result['message']).should.equal('Content is too short')
(result['message']).should.equal('Invalid content')
end
it 'should fail if content is very long' do
@ -39,7 +39,7 @@ describe '/user/login' do
})
(result['status']).should.equal('fail')
(result['message']).should.equal('Content is very long')
(result['message']).should.equal('Invalid content')
end
@ -50,7 +50,7 @@ describe '/user/login' do
})
(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')
end
end

View File

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