From 783deaf4605688c6df486eac04240c45f79a2fa6 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:04:11 +0200 Subject: [PATCH 01/29] [Fix:bug] composer install not working --- Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 203cc82..5d83756 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,12 +34,14 @@ CMD ["php-fpm"] # https://getcomposer.org/doc/03-cli.md#composer-allow-superuser ENV COMPOSER_ALLOW_SUPERUSER 1 -# Use prestissimo to speed up builds -RUN composer global require "hirak/prestissimo:^0.3" --prefer-dist --no-progress --no-suggest --optimize-autoloader --classmap-authoritative --no-interaction - ###> recipes ### ###< recipes ### COPY ./server . -RUN composer install \ No newline at end of file +# Use prestissimo to speed up builds +RUN composer global require "hirak/prestissimo:^0.3" --prefer-dist --no-progress --no-suggest --optimize-autoloader --classmap-authoritative --no-interaction + +RUN composer install + +RUN echo $(cat .env.dist) > .env \ No newline at end of file From f760fbc198e60755caadab04e2456edb64122504 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:04:20 +0200 Subject: [PATCH 02/29] README --- README.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 34628c6..0484506 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,31 @@ # interview-v1 -![](https://api.travis-ci.org/Sundowndev/interview-v1.svg) +Build status : ![](https://api.travis-ci.org/Sundowndev/interview-v1.svg) ## Description -1/ Développer en PHP une mini API REST avec output en json +1/ Develop a mini PHP REST API with json output -Cette api doit: +This api must manage 2 objects : +- User (id, name, email) +- Task (id, user_id, title, description, creation_date, status) - - Gérer 2 types d'objets: - User (id, name, email) - Task (id, user_id, title, description, creation_date, status) +Create API endpoints to recover a user or task data. (e.g /user/{id}) - - Mettre à disposition des endpoints permettant de récupérer les données d'un user et d'une task. (ex: /user/$id) - - - L'api doit être capable de manipuler la liste des taches associées à un utilisateur en offrant la possibilité de: - Récupérer cette liste de taches - Créer et ajouter une nouvelle tache - Supprimer une tache +L'api doit être capable de manipuler la liste des taches associées à un utilisateur en offrant la possibilité de: +- Fetch the latest tasks +- Create a task +- Delete a task En développant cette API, vous devez garder en tête qu'elle est susceptible d'évoluer (nouveaux retours, nouveaux attributs dans les objets) -2/ Développer un front en HtML/JS/CSS (pas de design nécessaire) +2/ Create a frontend client to call the API -Ce front doit communiquer avec l'api en ajax. -On doit pouvoir ajouter/supprimer un utilisateur -Gérer la liste des tâches d'un utilisateur (liste / ajout / suppression) +- The client must call the api using ajax +- We must be able to create/delete an user +- Manage user's tasks (read / add / delete) -(pas de framework) +(no framework) ## Installation and usage @@ -51,11 +49,19 @@ The architecture is made of a simple client -> server communication using Docker ## Security +To handle authentication feature, we use a CSRF and a http-only session cookie. + +As soon as the user provide valid credentials, we return a two tokens that will be needed for each request he will send to the API. + +For each request, the user send the CSRF token as GET/POST/DELETE/PUT parameter. The cookie is sent automatically. + +**Technical user story:** the user provide an username and password as POST parameter to /auth route. The credentials are checked in the database and if it's valid it returns a CSRF token and a token for the session cookie. The session is also stored in the database so at every client request, both tokens are checked and we can also identify the user through his tokens. + ## API endpoints | Method / Route | Resource | Description | | --------------------- | ------------------ | ------------ | -| `POST` /auth | Authentification | Connect and get an api key | +| `POST` /auth | Authentication | Connect and get an api key | | `GET` /tasks | Task | Get latest taks | | `GET` /tasks/{id} | Task | Get a task by given id | | `POST` /tasks | Task | Create a task | From 570728261481daaf63a02fa1bd05745645638929 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:04:35 +0200 Subject: [PATCH 03/29] App secret in env file --- server/.env.dist | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/.env.dist b/server/.env.dist index eb502d3..8e0b3ed 100644 --- a/server/.env.dist +++ b/server/.env.dist @@ -5,4 +5,5 @@ MYSQL_HOST=db MYSQL_PORT=3311 MYSQL_DBNAME=mysql ### Allow origin for the API ### -ALLOW_ORIGIN=^https?://localhost:?[0-9]*$ \ No newline at end of file +ALLOW_ORIGIN=^https?://localhost:?[0-9]*$ +APP_SECRET=04980744f74f4ec36ad5a9d5fec8876f \ No newline at end of file From 40197e0102e115c290a13cc30384c35821524917 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:04:53 +0200 Subject: [PATCH 04/29] Security service --- server/src/Service/Security.php | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 server/src/Service/Security.php diff --git a/server/src/Service/Security.php b/server/src/Service/Security.php new file mode 100644 index 0000000..39c198c --- /dev/null +++ b/server/src/Service/Security.php @@ -0,0 +1,81 @@ +session = new Session(); + } + + /** + * @return string + */ + public function generateToken() + { + $token = md5(uniqid(rand(), TRUE)); + + return $token; + } + + /** + * @param $cookie + * @return bool + */ + public function isLogged($cookie) + { + if (is_null($this->session->getSession($cookie))) { + return false; + } else { + return true; + } + } + + /** + * @param $csrf + * @param $cookie + * @return bool + */ + public function isValidCsrf($csrf, $cookie) + { + if (is_null($session = $this->session->getSession($cookie))) { + return false; + } + + return $session['csrf'] === $csrf; + } + + /** + * @param $password + * @return bool|string + */ + public function passwordHash($password) + { + return password_hash($password, PASSWORD_BCRYPT, [ + 'cost' => 12 + ]); + } + + /** + * @param $password + * @param $hash + * @return bool + */ + public function passwordVerify($password, $hash) + { + return \password_verify($password, $hash); + } +} \ No newline at end of file From eed75032e46dca52f68f62295755888fc05f7110 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:05:44 +0200 Subject: [PATCH 05/29] Session service --- server/src/Service/Session.php | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 server/src/Service/Session.php diff --git a/server/src/Service/Session.php b/server/src/Service/Session.php new file mode 100644 index 0000000..a3dceb8 --- /dev/null +++ b/server/src/Service/Session.php @@ -0,0 +1,51 @@ +db = new Database(); + } + + /** + * @param $user_id + * @param $csrf + * @param $cookie + */ + public function create($user_id, $csrf, $cookie) + { + $stmt = $this->db->getConnection()->prepare('INSERT INTO Session (user_id, csrf, cookie) VALUES(:user_id, :csrf, :cookie)'); + $stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT); + $stmt->bindParam(':title', $csrf, \PDO::PARAM_STR); + $stmt->bindParam(':description', $cookie, \PDO::PARAM_STR); + $stmt->execute(); + } + + /** + * @param $cookie + * @return mixed|null + */ + public function getSession($cookie) + { + $stmt = $this->db->getConnection()->prepare('SELECT * FROM Session WHERE cookie = :cookie'); + $stmt->bindParam(':cookie', $cookie); + $stmt->execute(); + + $session = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$session) { + return null; + } else { + return $session; + } + } +} \ No newline at end of file From 48fc15eceb70931023c4ad9df003490962bdcdc1 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:06:32 +0200 Subject: [PATCH 06/29] [Typo] Task controller --- server/src/Controller/TaskController.php | 64 +++++++++++++++++++----- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/server/src/Controller/TaskController.php b/server/src/Controller/TaskController.php index 24de6d0..650fb7d 100644 --- a/server/src/Controller/TaskController.php +++ b/server/src/Controller/TaskController.php @@ -5,23 +5,33 @@ namespace App\Controller; use App\Service\JsonResponse; use App\Service\Database; use App\Repository\TaskRepository; +use App\Service\Security; +use App\Service\Session; +/** + * Class TaskController + * @package App\Controller + */ class TaskController { - private $jsonResponse; private $db; + private $jsonResponse; + private $session; + private $security; public function __construct() { - $this->jsonResponse = new JsonResponse(); $this->db = new Database(); + $this->jsonResponse = new JsonResponse(); $this->repository = new TaskRepository($this->db); + $this->session = new Session(); + $this->security = new Security(); } /** * Get all tasks * - * Route: /task + * Route: /tasks * Method: GET */ public function getAll() @@ -36,7 +46,7 @@ class TaskController /** * Get all tasks * - * Route: /task/$id + * Route: /tasks/$id * Method: GET */ public function get($id) @@ -45,22 +55,46 @@ class TaskController $code = ($data != null) ? 200 : 404; $message = ($data != null) ? "Task found." : "Task not found."; - //var_dump($data); - print $this->jsonResponse->create($code, $message, $data); } /** * Create a task * - * Route: /task + * Route: /tasks * Method: POST */ public function post() { + if ($this->security->isLogged($_COOKIE['session'])) { + $code = 403; + $message = 'You are not authentified.'; + $data = []; + + print $this->jsonResponse->create($code, $message, $data); + exit(); + } + + if (empty($_POST['title']) || empty($_POST['description'])) { + $code = 400; + $message = 'Bad parameters.'; + $data = []; + + print $this->jsonResponse->create($code, $message, $data); + exit(); + } + + $task = $this->repository->create([ + 'user_id' => 1, + 'title' => $_POST['title'], + 'description' => $_POST['description'], + 'creation_date' => new \DateTime(), + 'status' => 1 + ]); + $code = 200; - $message = ""; - $data = []; + $message = 'Success!'; + $data = $task; print $this->jsonResponse->create($code, $message, $data); } @@ -68,7 +102,7 @@ class TaskController /** * Update a task * - * Route: /task/$id + * Route: /tasks/$id * Method: PUT */ public function put($id) @@ -83,13 +117,19 @@ class TaskController /** * Delete a task * - * Route: /task/$id + * Route: /tasks/$id * Method: DELETE */ public function delete($id) { + //verify auth + //verify csrf + //verify if author + + $this->repository->deleteById($id); + $code = 200; - $message = ""; + $message = "Task deleted."; $data = []; print $this->jsonResponse->create($code, $message, $data); From c19c3a52b2465320c6f8dc67adc0f70aec86ee28 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:07:53 +0200 Subject: [PATCH 07/29] [Annotations] Methods and variables annotations --- server/src/Service/Database.php | 14 +++++++++++++- server/src/Service/DotEnvParser.php | 4 ++++ server/src/Service/JsonResponse.php | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/server/src/Service/Database.php b/server/src/Service/Database.php index 3d3ce15..7c919a1 100644 --- a/server/src/Service/Database.php +++ b/server/src/Service/Database.php @@ -2,11 +2,18 @@ namespace App\Service; +/** + * Class Database + * @package App\Service + */ class Database { private $dotEnvParser; private $conn; + /** + * Database constructor. + */ public function __construct() { $this->dotEnvParser = new DotEnvParser(); @@ -29,13 +36,18 @@ class Database } } + /** + * Get the PDO connection instance + * + * @return \PDO + */ public function getConnection() { return $this->conn; } /** - * gestion des erreurs de retour d'execution de PDO + * Handle PDO execution errors * @param PDOStatement $stmt * @return void */ diff --git a/server/src/Service/DotEnvParser.php b/server/src/Service/DotEnvParser.php index ab3ab16..a589846 100644 --- a/server/src/Service/DotEnvParser.php +++ b/server/src/Service/DotEnvParser.php @@ -4,6 +4,10 @@ namespace App\Service; use josegonzalez\Dotenv\Loader; +/** + * Class DotEnvParser + * @package App\Service + */ class DotEnvParser { private $file; diff --git a/server/src/Service/JsonResponse.php b/server/src/Service/JsonResponse.php index c7d32ec..1642850 100644 --- a/server/src/Service/JsonResponse.php +++ b/server/src/Service/JsonResponse.php @@ -2,6 +2,10 @@ namespace App\Service; +/** + * Class JsonResponse + * @package App\Service + */ class JsonResponse { public function create(int $code, string $message = null, array $data = []) From 2b5eab09f1cac2b70976c8794242e0d0525018b5 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 12 Jul 2018 18:08:31 +0200 Subject: [PATCH 08/29] [Feature:Tasks] Create and update methods --- server/src/Repository/TaskRepository.php | 60 ++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/server/src/Repository/TaskRepository.php b/server/src/Repository/TaskRepository.php index 0ea9969..77077e6 100644 --- a/server/src/Repository/TaskRepository.php +++ b/server/src/Repository/TaskRepository.php @@ -1,20 +1,36 @@ db = $db; $this->tableName = 'Task'; } + /** + * @return mixed + */ public function findAll() { $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' ORDER BY id DESC'); @@ -23,6 +39,10 @@ class TaskRepository return $stmt->fetchAll(\PDO::FETCH_ASSOC); } + /** + * @param $id + * @return null + */ public function findOneById($id) { $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' WHERE id = :id'); @@ -38,18 +58,50 @@ class TaskRepository } } + /** + * @param $data + * @return mixed + */ public function create($data) { - // + $stmt = $this->db->getConnection()->prepare('INSERT INTO ' . $this->tableName . ' (user_id, title, description, creation_date, status) VALUES(:user_id, :title, :description, :creation_date, :status)'); + $stmt->bindParam(':user_id', $data['user_id'], \PDO::PARAM_INT); + $stmt->bindParam(':title', $data['title'], \PDO::PARAM_STR); + $stmt->bindParam(':description', $data['description'], \PDO::PARAM_STR); + $stmt->bindParam(':creation_date', $data['creation_date']); + $stmt->bindParam(':status', $data['status'], \PDO::PARAM_INT); + $stmt->execute(); + + return $data; } + /** + * @param $id + * @param $data + * @return mixed + */ public function updateById($id, $data) { - // + $task = $this->findOneById($id); + + $stmt = $this->db->getConnection()->prepare('UPDATE ' . $this->tableName . ' SET user_id = :user_id, title = :title, description = :description, creation_date = :creation_date, status = :status'); + $stmt->bindParam(':user_id', $data['user_id'] ?? $task['user_id'], \PDO::PARAM_INT); + $stmt->bindParam(':title', $data['title'] ?? $task['title'], \PDO::PARAM_STR); + $stmt->bindParam(':description', $data['description'] ?? $task['description'], \PDO::PARAM_STR); + $stmt->bindParam(':creation_date', $data['creation_date'] ?? $task['creation_date']); + $stmt->bindParam(':status', $data['status'] ?? $task['status'], \PDO::PARAM_INT); + $stmt->execute(); + + return $data; } + /** + * @param $id + */ public function deleteById($id) { - // + $stmt = $this->db->getConnection()->prepare('DELETE FROM ' . $this->tableName . ' WHERE id = :id'); + $stmt->bindParam(':id', $id, \PDO::PARAM_INT); + $stmt->execute(); } } \ No newline at end of file From 86a136e4b41619df8daf87ceee0f5ff82970207a Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:31:11 +0200 Subject: [PATCH 09/29] Session controller --- server/src/Controller/SessionController.php | 100 ++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 server/src/Controller/SessionController.php diff --git a/server/src/Controller/SessionController.php b/server/src/Controller/SessionController.php new file mode 100644 index 0000000..9216276 --- /dev/null +++ b/server/src/Controller/SessionController.php @@ -0,0 +1,100 @@ +db = new Database(); + $this->request = new Request(); + $this->jsonResponse = new JsonResponse(); + $this->session = new Session($this->db, $this->jsonResponse); + $this->security = $this->session->security; + $this->userRepository = new UserRepository($this->db); + } + + /** + * Sign in route + */ + public function auth() + { + $content = $this->request->getContentAsArray(); + + if (empty($content['username']) || empty($content['password'])) { + print $this->jsonResponse->create(400, 'Please provide an username and password.'); + exit(); + } + + $user = $this->userRepository->findOneByUsername($content['username']); + + if (!$this->security->passwordVerify($content['password'], $user['password'])) { + print $this->jsonResponse->create(403, 'Bad credentials.'); + exit(); + } + + $token = $this->security->generateToken(); + + $expire_at = new \DateTime(); + $expire_at->add(new \DateInterval('P1D')); // Expire in 1 day + + $stmt = $this->db->getConnection()->prepare('INSERT INTO Session (`user_id`, `token`, `issued_at`, `expire_at`) VALUES(:user_id, :token, NOW(), :expire_at)'); + $stmt->bindParam(':user_id', $user['id']); + $stmt->bindParam(':token', $token); + $stmt->bindParam(':expire_at', date_format($expire_at, 'Y-m-d')); + $stmt->execute(); + + print $this->jsonResponse->create(200, 'Welcome ' . $user['name'], [ + 'jwt_token' => $token, + 'expire_at' => $expire_at, + ]); + } + + /** + * Register route + */ + public function signup() + { + $content = json_decode(trim(file_get_contents("php://input")), true); + + if (empty($content['username']) || empty($content['email']) || empty($content['password'])) { + print $this->jsonResponse->create(400, 'Please provide an username, email and password.'); + exit(); + } + + $user = [ + 'username' => $content['username'], + 'email' => $content['email'], + 'password' => $this->security->passwordHash($content['password']), + ]; + + $stmt = $this->db->getConnection()->prepare('INSERT INTO User (`name`, `email`, `password`) VALUES(:name, :email, :password)'); + $stmt->bindParam(':name', $user['username']); + $stmt->bindParam(':email', $user['email']); + $stmt->bindParam(':password', $user['password']); + $stmt->execute(); + + print $this->jsonResponse->create(200, 'Success! Now send your credentials to /auth to sign in.', [ + 'username' => $user['username'], + 'email' => $user['email'], + ]); + } + + public function signout() + { + // logout + } +} \ No newline at end of file From 17007e73cd80f53d23c10e58bdf9bfe4e59fe1e3 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:31:22 +0200 Subject: [PATCH 10/29] User controller --- server/src/Controller/UserController.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 server/src/Controller/UserController.php diff --git a/server/src/Controller/UserController.php b/server/src/Controller/UserController.php new file mode 100644 index 0000000..ffb00de --- /dev/null +++ b/server/src/Controller/UserController.php @@ -0,0 +1,8 @@ + Date: Sun, 22 Jul 2018 02:31:32 +0200 Subject: [PATCH 11/29] Request service --- server/src/Service/Request.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 server/src/Service/Request.php diff --git a/server/src/Service/Request.php b/server/src/Service/Request.php new file mode 100644 index 0000000..5053286 --- /dev/null +++ b/server/src/Service/Request.php @@ -0,0 +1,15 @@ + Date: Sun, 22 Jul 2018 02:31:58 +0200 Subject: [PATCH 12/29] Task repository --- server/src/Repository/TaskRepository.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/Repository/TaskRepository.php b/server/src/Repository/TaskRepository.php index 77077e6..392d39c 100644 --- a/server/src/Repository/TaskRepository.php +++ b/server/src/Repository/TaskRepository.php @@ -1,7 +1,6 @@ db->getConnection()->prepare('INSERT INTO ' . $this->tableName . ' (user_id, title, description, creation_date, status) VALUES(:user_id, :title, :description, :creation_date, :status)'); + $stmt = $this->db->getConnection()->prepare('INSERT INTO ' . $this->tableName . ' (user_id, title, description, creation_date, status) VALUES(:user_id, :title, :description, NOW(), :status)'); $stmt->bindParam(':user_id', $data['user_id'], \PDO::PARAM_INT); $stmt->bindParam(':title', $data['title'], \PDO::PARAM_STR); $stmt->bindParam(':description', $data['description'], \PDO::PARAM_STR); - $stmt->bindParam(':creation_date', $data['creation_date']); $stmt->bindParam(':status', $data['status'], \PDO::PARAM_INT); $stmt->execute(); From 5d83adb0b37a00042cb2ff248f0d4e774bede7f0 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:32:23 +0200 Subject: [PATCH 13/29] Env variables --- server/.env.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/.env.dist b/server/.env.dist index 8e0b3ed..3ac0ac2 100644 --- a/server/.env.dist +++ b/server/.env.dist @@ -3,7 +3,7 @@ MYSQL_USER=mysql MYSQL_PASS=mysql MYSQL_HOST=db MYSQL_PORT=3311 -MYSQL_DBNAME=mysql +MYSQL_DBNAME=app1 ### Allow origin for the API ### -ALLOW_ORIGIN=^https?://localhost:?[0-9]*$ +ALLOW_ORIGIN=http://localhost:8000 APP_SECRET=04980744f74f4ec36ad5a9d5fec8876f \ No newline at end of file From c176e32f8972aae3dbebdcb8110c61e894c026b8 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:32:40 +0200 Subject: [PATCH 14/29] Routes --- server/app/routes.php | 67 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/server/app/routes.php b/server/app/routes.php index 3bbd311..151f48e 100644 --- a/server/app/routes.php +++ b/server/app/routes.php @@ -1,32 +1,85 @@ setNamespace('\App\Controller'); -$router->before('GET|POST', '/.*', function() use ($router) { +$router->before('GET|POST', '/.*', function () use ($router) { # This will be always executed + $dotEnvParser = new DotEnvParser(); + $dotEnvParser->run(); + + $jsonResponse = new JsonResponse(); + + if ($_SERVER['HTTP_ACCEPT'] !== 'application/json') { + $code = 400; + $message = 'Accept header is not set to "application/json".'; + print $jsonResponse->create($code, $message, []); + die(); + } elseif ($_SERVER['REQUEST_METHOD'] != 'GET' && $_SERVER['CONTENT_TYPE'] !== 'application/json') { + $code = 400; + $message = 'Content-type header is not set to "application/json".'; + print $jsonResponse->create($code, $message, []); + die(); + }/*elseif($_SERVER['HTTP_ORIGIN'] !== getenv('ALLOW_ORIGIN')) { + $code = 403; + $message = 'Unallowed cross origin.'; + print $jsonResponse->create($code, $message, []); + die(); + }*/ }); +/** + * API index + */ $router->get('/', 'DefaultController@index'); +/** + * Session handling routes + */ +$router->post('/auth', 'SessionController@auth'); +$router->post('/register', 'SessionController@signup'); +$router->post('/logout', 'SessionController@signout'); + +/** + * Task resource + */ $router->mount('/tasks', function () use ($router) { - // get all tasks + // Get all tasks $router->get('/', 'TaskController@getAll'); - // get one task + // Get one task $router->get('/(\d+)', 'TaskController@get'); - // create a task + // Create a task $router->post('/', 'TaskController@post'); - // update a task + // Update a task $router->put('/(\d+)', 'TaskController@put'); - // delete a task + // Delete a task $router->delete('/(\d+)', 'TaskController@delete'); }); +/** + * User resource + */ $router->mount('/users', function () use ($router) { - // endpoints for user + // Create user (register) + $router->post('/', 'DefaultController@index'); + + // Get your own account data + $router->get('/me', 'DefaultController@index'); + + // Get one user + $router->get('/(\d+)', 'DefaultController@index'); + + // Get one task's tasks + $router->get('/(\d+)/tasks', 'DefaultController@index'); }); +/** + * 404 error response + */ $router->set404('DefaultController@error'); \ No newline at end of file From e32435f5bbbda2c2319fc821a07e73af3486d5c0 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:33:22 +0200 Subject: [PATCH 15/29] Avoid circular reference in controllers & services --- server/src/Controller/TaskController.php | 45 ++++++++++++++---------- server/src/Service/Database.php | 32 ++++++++--------- server/src/Service/DotEnvParser.php | 36 ++++++------------- server/src/Service/JsonResponse.php | 2 ++ server/src/Service/Security.php | 38 +++++++++++--------- server/src/Service/Session.php | 25 ++++++++----- 6 files changed, 91 insertions(+), 87 deletions(-) diff --git a/server/src/Controller/TaskController.php b/server/src/Controller/TaskController.php index 650fb7d..644df41 100644 --- a/server/src/Controller/TaskController.php +++ b/server/src/Controller/TaskController.php @@ -5,7 +5,7 @@ namespace App\Controller; use App\Service\JsonResponse; use App\Service\Database; use App\Repository\TaskRepository; -use App\Service\Security; +use App\Service\Request; use App\Service\Session; /** @@ -15,17 +15,20 @@ use App\Service\Session; class TaskController { private $db; + private $request; private $jsonResponse; private $session; private $security; + private $repository; public function __construct() { $this->db = new Database(); + $this->request = new Request(); $this->jsonResponse = new JsonResponse(); $this->repository = new TaskRepository($this->db); - $this->session = new Session(); - $this->security = new Security(); + $this->session = new Session($this->db, $this->jsonResponse); + $this->security = $this->session->security; } /** @@ -44,7 +47,7 @@ class TaskController } /** - * Get all tasks + * Get task by id * * Route: /tasks/$id * Method: GET @@ -66,29 +69,25 @@ class TaskController */ public function post() { - if ($this->security->isLogged($_COOKIE['session'])) { - $code = 403; - $message = 'You are not authentified.'; - $data = []; - - print $this->jsonResponse->create($code, $message, $data); + if (!$this->security->isLogged()) { + print $this->security->NotAllowedRequest(); exit(); } - if (empty($_POST['title']) || empty($_POST['description'])) { + $content = $this->request->getContentAsArray(); + + if (empty($content['title']) || empty($content['description'])) { $code = 400; $message = 'Bad parameters.'; - $data = []; - print $this->jsonResponse->create($code, $message, $data); + print $this->jsonResponse->create($code, $message); exit(); } $task = $this->repository->create([ 'user_id' => 1, - 'title' => $_POST['title'], - 'description' => $_POST['description'], - 'creation_date' => new \DateTime(), + 'title' => $content['title'], + 'description' => $content['description'], 'status' => 1 ]); @@ -122,9 +121,17 @@ class TaskController */ public function delete($id) { - //verify auth - //verify csrf - //verify if author + if (!$this->security->isLogged()) { + print $this->security->NotAllowedRequest(); + exit(); + } + + $task = $this->repository->findOneById($id); + + if ($task['user_id'] !== 1) { + print $this->security->NotAllowedRequest(); + exit(); + } $this->repository->deleteById($id); diff --git a/server/src/Service/Database.php b/server/src/Service/Database.php index 7c919a1..3d5c294 100644 --- a/server/src/Service/Database.php +++ b/server/src/Service/Database.php @@ -8,47 +8,45 @@ namespace App\Service; */ class Database { - private $dotEnvParser; private $conn; + private $dsn; + private $options; /** * Database constructor. */ public function __construct() { - $this->dotEnvParser = new DotEnvParser(); - $this->dotEnvParser - ->parse() - ->toEnv() - ->toArray(); + $this->conn = null; - $dsn = "mysql:host=" . $_ENV['MYSQL_HOST'] . ";dbname=" . $_ENV['MYSQL_DBNAME']; - $options = array( + $this->dsn = "mysql:host=" . getenv('MYSQL_HOST') . ":".getenv('MYSQL_PORT').";dbname=" . getenv('MYSQL_DBNAME'); + $this->options = array( \PDO::ATTR_PERSISTENT => true, \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION ); - - try { - $this->conn = new \PDO($dsn, $_ENV['MYSQL_USER'], $_ENV['MYSQL_PASS'], $options); - } //catch any errors - catch (\PDOException $e) { - $this->error = $e->getMessage(); - } } /** * Get the PDO connection instance - * * @return \PDO */ public function getConnection() { + if (is_null($this->conn)) { + try { + $this->conn = new \PDO($this->dsn, getenv('MYSQL_USER'), getenv('MYSQL_PASS'), $this->options); + } //catch any errors + catch (\PDOException $e) { + exit($e->getMessage()); + } + } + return $this->conn; } /** * Handle PDO execution errors - * @param PDOStatement $stmt + * @param \PDOStatement $stmt * @return void */ public function errorHandler(\PDOStatement $stmt) : void diff --git a/server/src/Service/DotEnvParser.php b/server/src/Service/DotEnvParser.php index a589846..1232735 100644 --- a/server/src/Service/DotEnvParser.php +++ b/server/src/Service/DotEnvParser.php @@ -2,7 +2,10 @@ namespace App\Service; -use josegonzalez\Dotenv\Loader; +//use M1\Env\Parser; +//use Dotenv\Dotenv; +//use \Jsefton\DotEnv\Parser; +use Codervio\Envmanager\Envparser; /** * Class DotEnvParser @@ -11,37 +14,18 @@ use josegonzalez\Dotenv\Loader; class DotEnvParser { private $file; - private $loader; + private $parser; public function __construct() { $this->file = __DIR__ . '/../../.env'; - $this->loader = new Loader($this->file); + + $this->parser = new Envparser($this->file); + $this->parser->load(); } - /** - * Parse the .env file - * @return bool|Loader - */ - public function parse() + public function run() { - return $this->loader->parse(); - } - - /** - * Send the parsed .env file to the $_ENV variable - * @return bool|Loader - */ - public function toEnv() - { - return $this->loader->toEnv(); - } - - /** - * @return array|null - */ - public function toArray() - { - return $this->loader->toArray(); + $this->parser->run(); } } \ No newline at end of file diff --git a/server/src/Service/JsonResponse.php b/server/src/Service/JsonResponse.php index 1642850..d314514 100644 --- a/server/src/Service/JsonResponse.php +++ b/server/src/Service/JsonResponse.php @@ -16,6 +16,8 @@ class JsonResponse 'data' => $data ]; + header('Access-Control-Allow-Origin: ' . getenv('ALLOW_ORIGIN')); + header('Accept: application/json'); header('Content-Type: application/json'); http_response_code($code); diff --git a/server/src/Service/Security.php b/server/src/Service/Security.php index 39c198c..b019321 100644 --- a/server/src/Service/Security.php +++ b/server/src/Service/Security.php @@ -13,12 +13,24 @@ class Security */ private $session; + /** + * @var JsonResponse + */ + private $jsonResponse; + + /** + * @var $secret_key + */ + private $secret_key; + /** * Security constructor. */ - public function __construct() + public function __construct(Session $session, JsonResponse $jsonResponse) { - $this->session = new Session(); + $this->session = $session; + $this->jsonResponse = $jsonResponse; + $this->secret_key = getenv('APP_SECRET'); } /** @@ -35,27 +47,21 @@ class Security * @param $cookie * @return bool */ - public function isLogged($cookie) + public function isLogged() { - if (is_null($this->session->getSession($cookie))) { - return false; - } else { - return true; - } + return false; } /** - * @param $csrf - * @param $cookie - * @return bool + * @return string */ - public function isValidCsrf($csrf, $cookie) + public function NotAllowedRequest() { - if (is_null($session = $this->session->getSession($cookie))) { - return false; - } + $code = 403; + $message = 'You are not allowed to perform this request.'; + $data = []; - return $session['csrf'] === $csrf; + return $this->jsonResponse->create($code, $message, $data); } /** diff --git a/server/src/Service/Session.php b/server/src/Service/Session.php index a3dceb8..090efb6 100644 --- a/server/src/Service/Session.php +++ b/server/src/Service/Session.php @@ -8,12 +8,16 @@ namespace App\Service; */ class Session { + private $db; + public $security; + /** * Session constructor. */ - public function __construct() + public function __construct(Database $database, JsonResponse $jsonResponse) { - $this->db = new Database(); + $this->db = $database; + $this->security = new Security($this, $jsonResponse); } /** @@ -21,12 +25,15 @@ class Session * @param $csrf * @param $cookie */ - public function create($user_id, $csrf, $cookie) + public function create($user_id) { - $stmt = $this->db->getConnection()->prepare('INSERT INTO Session (user_id, csrf, cookie) VALUES(:user_id, :csrf, :cookie)'); + $token = $this->security->generateToken(); + $expire_at = new \DateTime(); + + $stmt = $this->db->getConnection()->prepare('INSERT INTO Session (user_id, token, issued_at, expire_at) VALUES(:user_id, :token, NOW(), :expire_at)'); $stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT); - $stmt->bindParam(':title', $csrf, \PDO::PARAM_STR); - $stmt->bindParam(':description', $cookie, \PDO::PARAM_STR); + $stmt->bindParam(':token', $token, \PDO::PARAM_STR); + $stmt->bindParam(':expire_at', $expire_at); $stmt->execute(); } @@ -34,10 +41,10 @@ class Session * @param $cookie * @return mixed|null */ - public function getSession($cookie) + public function getSession($token) { - $stmt = $this->db->getConnection()->prepare('SELECT * FROM Session WHERE cookie = :cookie'); - $stmt->bindParam(':cookie', $cookie); + $stmt = $this->db->getConnection()->prepare('SELECT * FROM Session WHERE token = :token'); + $stmt->bindParam(':token', $token); $stmt->execute(); $session = $stmt->fetch(\PDO::FETCH_ASSOC); From b561b284a5e0ce990eb8897a7bf20399cd073830 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:33:37 +0200 Subject: [PATCH 16/29] Session repository --- server/src/Repository/SessionRepository.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 server/src/Repository/SessionRepository.php diff --git a/server/src/Repository/SessionRepository.php b/server/src/Repository/SessionRepository.php new file mode 100644 index 0000000..e69de29 From a1f99892af0848c6fdb1da42605f79c2f66294a1 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:33:49 +0200 Subject: [PATCH 17/29] User repository --- server/src/Repository/UserRepository.php | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 server/src/Repository/UserRepository.php diff --git a/server/src/Repository/UserRepository.php b/server/src/Repository/UserRepository.php new file mode 100644 index 0000000..09f2d87 --- /dev/null +++ b/server/src/Repository/UserRepository.php @@ -0,0 +1,74 @@ +db = $db; + $this->tableName = 'User'; + } + + /** + * @return mixed + */ + public function findAll() + { + $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' ORDER BY id DESC'); + $stmt->execute(); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + /** + * @param $id + * @return null + */ + public function findOneById($id) + { + $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' WHERE id = :id'); + $stmt->bindParam(':id', $id, \PDO::PARAM_INT); + $stmt->execute(); + + $user = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$user) { + return null; + } else { + return $user; + } + } + + public function findOneByUsername($username) + { + $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' WHERE name = :username'); + $stmt->bindParam(':username', $username, \PDO::PARAM_INT); + $stmt->execute(); + + $user = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$user) { + return null; + } else { + return $user; + } + } +} \ No newline at end of file From 37904719fae9107691b5fdfa82f2e6263b961a3d Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:34:43 +0200 Subject: [PATCH 18/29] Database structure dump --- server/references/database.sql | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 server/references/database.sql diff --git a/server/references/database.sql b/server/references/database.sql new file mode 100644 index 0000000..c372352 --- /dev/null +++ b/server/references/database.sql @@ -0,0 +1,44 @@ +-- Adminer 4.6.3 MySQL dump + +SET NAMES utf8; +SET time_zone = '+00:00'; +SET foreign_key_checks = 0; +SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; + +DROP DATABASE IF EXISTS `app1`; +CREATE DATABASE `app1` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `app1`; + +DROP TABLE IF EXISTS `Session`; +CREATE TABLE `Session` ( + `user_id` int(11) NOT NULL, + `token` varchar(255) NOT NULL, + `issued_at` datetime NOT NULL, + `expire_at` datetime NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + + +DROP TABLE IF EXISTS `Task`; +CREATE TABLE `Task` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + `creation_date` datetime DEFAULT NULL, + `status` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `Task_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + + +DROP TABLE IF EXISTS `User`; +CREATE TABLE `User` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + + +-- 2018-07-22 00:34:19 From 8c2abfa1e8bfb5d4ac2a9799a0bf184db0666e1a Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:34:59 +0200 Subject: [PATCH 19/29] Adminer image for dev env --- docker-compose.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index c25a871..0987f5c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,7 +7,7 @@ services: - "3311:3306" environment: MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: mysql + MYSQL_DATABASE: app1 MYSQL_USER: mysql MYSQL_PASSWORD: mysql @@ -42,3 +42,10 @@ services: - ./docker/httpd/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro ports: - '443:443' + + # For development purposes + adminer: + image: adminer + restart: always + ports: + - 8080:8080 \ No newline at end of file From 1d077f988df0f156ce8bab1750fa4c09e438c716 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 22 Jul 2018 02:37:14 +0200 Subject: [PATCH 20/29] Env file parser dependency --- server/composer.json | 2 +- server/composer.lock | 108 +++++++++++-------------------------------- 2 files changed, 28 insertions(+), 82 deletions(-) diff --git a/server/composer.json b/server/composer.json index ec1a615..a8c8b1c 100644 --- a/server/composer.json +++ b/server/composer.json @@ -5,7 +5,7 @@ "license": "WTFPL", "require": { "bramus/router": "~1.3", - "josegonzalez/dotenv": "~3.2.0" + "codervio/envmanager": "^1.7" }, "autoload": { "psr-4": { diff --git a/server/composer.lock b/server/composer.lock index 598d4f4..fe4369e 100644 --- a/server/composer.lock +++ b/server/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "759257fb9b4837a4e886866e36a4c233", + "content-hash": "4ac0df3751e41700bc67b558c17957c9", "packages": [ { "name": "bramus/router", @@ -53,88 +53,29 @@ "time": "2017-12-21T20:37:23+00:00" }, { - "name": "josegonzalez/dotenv", - "version": "dev-master", + "name": "codervio/envmanager", + "version": "1.7", "source": { "type": "git", - "url": "https://github.com/josegonzalez/php-dotenv.git", - "reference": "bc1677b827829f1e1db4a55b8283b4b836617499" + "url": "https://github.com/Codervio/Envmanager.git", + "reference": "6b74977f2a048fd2afac144032ccda2808ed2acb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/josegonzalez/php-dotenv/zipball/bc1677b827829f1e1db4a55b8283b4b836617499", - "reference": "bc1677b827829f1e1db4a55b8283b4b836617499", + "url": "https://api.github.com/repos/Codervio/Envmanager/zipball/6b74977f2a048fd2afac144032ccda2808ed2acb", + "reference": "6b74977f2a048fd2afac144032ccda2808ed2acb", "shasum": "" }, "require": { - "m1/env": "2.*", - "php": ">=5.5.0" + "php": "^7.0" }, "require-dev": { - "php-mock/php-mock-phpunit": "^1.1", - "satooshi/php-coveralls": "1.*", - "squizlabs/php_codesniffer": "2.*" - }, - "type": "library", - "autoload": { - "psr-0": { - "josegonzalez\\Dotenv": [ - "src", - "tests" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jose Diaz-Gonzalez", - "email": "dotenv@josegonzalez.com", - "homepage": "http://josediazgonzalez.com", - "role": "Maintainer" - } - ], - "description": "dotenv file parsing for PHP", - "homepage": "https://github.com/josegonzalez/php-dotenv", - "keywords": [ - "configuration", - "dotenv", - "php" - ], - "time": "2017-11-27T14:55:32+00:00" - }, - { - "name": "m1/env", - "version": "2.1.2", - "source": { - "type": "git", - "url": "https://github.com/m1/Env.git", - "reference": "294addeedf15e1149eeb96ec829f2029d2017d39" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/m1/Env/zipball/294addeedf15e1149eeb96ec829f2029d2017d39", - "reference": "294addeedf15e1149eeb96ec829f2029d2017d39", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*", - "scrutinizer/ocular": "~1.1", - "squizlabs/php_codesniffer": "^2.3" - }, - "suggest": { - "josegonzalez/dotenv": "For loading of .env", - "m1/vars": "For loading of configs" + "phpunit/phpunit": "6.0.*" }, "type": "library", "autoload": { "psr-4": { - "M1\\Env\\": "src" + "Codervio\\Envmanager\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -143,33 +84,38 @@ ], "authors": [ { - "name": "Miles Croxford", - "email": "hello@milescroxford.com", - "homepage": "http://milescroxford.com", - "role": "Developer" + "name": "Marin Sagovac", + "email": "marin@sagovac.com" + }, + { + "name": "Codervio Community", + "homepage": "http://codervio.com" } ], - "description": "Env is a lightweight library bringing .env file parser compatibility to PHP. In short - it enables you to read .env files with PHP.", - "homepage": "https://github.com/m1/Env", + "description": "Codervio Environment manager", + "homepage": "http://codervio.com", "keywords": [ ".env", + "array-env", "config", + "dev-tools", + "dot-env", "dotenv", + "dotenv-editor", "env", + "environment", + "framework", "loader", - "m1", "parser", - "support" + "php" ], - "time": "2018-06-19T18:55:08+00:00" + "time": "2018-03-07T23:55:50+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "josegonzalez/dotenv": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": [], From c01b3a5f736ce37d75a6361a91975176d913b1aa Mon Sep 17 00:00:00 2001 From: Raphael Cerveaux Date: Sun, 22 Jul 2018 17:01:40 +0200 Subject: [PATCH 21/29] Before route methods --- server/app/routes.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/app/routes.php b/server/app/routes.php index 151f48e..6e178ed 100644 --- a/server/app/routes.php +++ b/server/app/routes.php @@ -5,7 +5,7 @@ use App\Service\JsonResponse; $router->setNamespace('\App\Controller'); -$router->before('GET|POST', '/.*', function () use ($router) { +$router->before('GET|POST|PUT|DELETE', '/.*', function () use ($router) { # This will be always executed $dotEnvParser = new DotEnvParser(); $dotEnvParser->run(); @@ -16,17 +16,17 @@ $router->before('GET|POST', '/.*', function () use ($router) { $code = 400; $message = 'Accept header is not set to "application/json".'; print $jsonResponse->create($code, $message, []); - die(); + exit(); } elseif ($_SERVER['REQUEST_METHOD'] != 'GET' && $_SERVER['CONTENT_TYPE'] !== 'application/json') { $code = 400; $message = 'Content-type header is not set to "application/json".'; print $jsonResponse->create($code, $message, []); - die(); - }/*elseif($_SERVER['HTTP_ORIGIN'] !== getenv('ALLOW_ORIGIN')) { + exit(); + }/* elseif ($_SERVER['HTTP_ORIGIN'] !== getenv('ALLOW_ORIGIN')) { $code = 403; - $message = 'Unallowed cross origin.'; + $message = 'Unallowed origin.'; print $jsonResponse->create($code, $message, []); - die(); + exit(); }*/ }); @@ -82,4 +82,4 @@ $router->mount('/users', function () use ($router) { /** * 404 error response */ -$router->set404('DefaultController@error'); \ No newline at end of file +$router->set404('DefaultController@error'); From c7009e1d3f9bbc38a37434905b4e38b2e57c8396 Mon Sep 17 00:00:00 2001 From: Raphael Cerveaux Date: Mon, 23 Jul 2018 10:06:02 +0200 Subject: [PATCH 22/29] Remove old .env file parsers --- server/src/Service/DotEnvParser.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/Service/DotEnvParser.php b/server/src/Service/DotEnvParser.php index 1232735..d760c1d 100644 --- a/server/src/Service/DotEnvParser.php +++ b/server/src/Service/DotEnvParser.php @@ -2,9 +2,6 @@ namespace App\Service; -//use M1\Env\Parser; -//use Dotenv\Dotenv; -//use \Jsefton\DotEnv\Parser; use Codervio\Envmanager\Envparser; /** @@ -28,4 +25,4 @@ class DotEnvParser { $this->parser->run(); } -} \ No newline at end of file +} From a09d63dd898b88841bf084cc47702b75a1aad0bf Mon Sep 17 00:00:00 2001 From: Raphael Cerveaux Date: Mon, 23 Jul 2018 23:46:05 +0200 Subject: [PATCH 23/29] [Documentation] Security --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0484506..79d62a7 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,15 @@ The architecture is made of a simple client -> server communication using Docker ## Security -To handle authentication feature, we use a CSRF and a http-only session cookie. +To handle authentication feature, we use JWT authentication. -As soon as the user provide valid credentials, we return a two tokens that will be needed for each request he will send to the API. +JSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. [Source](https://jwt.io/introduction/) -For each request, the user send the CSRF token as GET/POST/DELETE/PUT parameter. The cookie is sent automatically. +As soon as the user provide valid credentials, we return a JWT token that will be needed for each request the client will send to the API. -**Technical user story:** the user provide an username and password as POST parameter to /auth route. The credentials are checked in the database and if it's valid it returns a CSRF token and a token for the session cookie. The session is also stored in the database so at every client request, both tokens are checked and we can also identify the user through his tokens. +For each request, the user send the JWT token as parameter. + +![JWT explained](https://cdn-images-1.medium.com/max/1400/1*SSXUQJ1dWjiUrDoKaaiGLA.png) ## API endpoints From e467fb29540f4606c7b46355ac801b04766aaff8 Mon Sep 17 00:00:00 2001 From: Raphael Cerveaux Date: Thu, 26 Jul 2018 12:42:35 +0200 Subject: [PATCH 24/29] [Translation] Description --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79d62a7..84266fa 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ This api must manage 2 objects : Create API endpoints to recover a user or task data. (e.g /user/{id}) -L'api doit être capable de manipuler la liste des taches associées à un utilisateur en offrant la possibilité de: +The API must be able to manage users tasks and create the endpoints to: - Fetch the latest tasks - Create a task - Delete a task -En développant cette API, vous devez garder en tête qu'elle est susceptible d'évoluer (nouveaux retours, nouveaux attributs dans les objets) +While developing this API, you must keep in mind it can evolve at any moment (new resources, new properties in objects ...). 2/ Create a frontend client to call the API From a257bf0b2bc1e37c4bde4f5c2b25d82de099062c Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 26 Jul 2018 15:47:43 +0200 Subject: [PATCH 25/29] env.dist --- server/.env.dist | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/.env.dist b/server/.env.dist index 3ac0ac2..8a24a04 100644 --- a/server/.env.dist +++ b/server/.env.dist @@ -1,9 +1,9 @@ ### Environment file ### -MYSQL_USER=mysql -MYSQL_PASS=mysql +MYSQL_USER=root +MYSQL_PASS=root MYSQL_HOST=db -MYSQL_PORT=3311 +MYSQL_PORT=3306 MYSQL_DBNAME=app1 ### Allow origin for the API ### -ALLOW_ORIGIN=http://localhost:8000 +ALLOW_ORIGIN=^https?://localhost:?[0-9]*$ APP_SECRET=04980744f74f4ec36ad5a9d5fec8876f \ No newline at end of file From 95a0f3c553c337064a26bb3c2540af686ee84819 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 26 Jul 2018 15:48:03 +0200 Subject: [PATCH 26/29] Session routes --- server/app/routes.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server/app/routes.php b/server/app/routes.php index 6e178ed..eef953c 100644 --- a/server/app/routes.php +++ b/server/app/routes.php @@ -35,12 +35,18 @@ $router->before('GET|POST|PUT|DELETE', '/.*', function () use ($router) { */ $router->get('/', 'DefaultController@index'); +/** + * 404 error response + */ +$router->set404('DefaultController@error'); + /** * Session handling routes */ $router->post('/auth', 'SessionController@auth'); -$router->post('/register', 'SessionController@signup'); +$router->post('/signup', 'SessionController@signup'); $router->post('/logout', 'SessionController@signout'); +$router->get('/me', 'SessionController@me'); /** * Task resource @@ -69,9 +75,6 @@ $router->mount('/users', function () use ($router) { // Create user (register) $router->post('/', 'DefaultController@index'); - // Get your own account data - $router->get('/me', 'DefaultController@index'); - // Get one user $router->get('/(\d+)', 'DefaultController@index'); @@ -79,7 +82,3 @@ $router->mount('/users', function () use ($router) { $router->get('/(\d+)/tasks', 'DefaultController@index'); }); -/** - * 404 error response - */ -$router->set404('DefaultController@error'); From 3657aef20853465956fcc53b9a7c3476fcb02aae Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 26 Jul 2018 15:49:29 +0200 Subject: [PATCH 27/29] Session authentication and logout --- server/src/Controller/SessionController.php | 73 ++++++---- server/src/Repository/SessionRepository.php | 142 ++++++++++++++++++++ server/src/Service/Session.php | 29 ++-- 3 files changed, 202 insertions(+), 42 deletions(-) diff --git a/server/src/Controller/SessionController.php b/server/src/Controller/SessionController.php index 9216276..01ebe63 100644 --- a/server/src/Controller/SessionController.php +++ b/server/src/Controller/SessionController.php @@ -2,6 +2,7 @@ namespace App\Controller; +use App\Repository\SessionRepository; use App\Repository\UserRepository; use App\Service\Database; use App\Service\JsonResponse; @@ -12,6 +13,7 @@ class SessionController { private $db; private $jsonResponse; + private $sessionRepository; private $request; private $session; private $security; @@ -24,6 +26,7 @@ class SessionController $this->jsonResponse = new JsonResponse(); $this->session = new Session($this->db, $this->jsonResponse); $this->security = $this->session->security; + $this->sessionRepository = new SessionRepository($this->db, $this->security); $this->userRepository = new UserRepository($this->db); } @@ -32,69 +35,89 @@ class SessionController */ public function auth() { - $content = $this->request->getContentAsArray(); + $body = $this->request->getContent()->jsonToArray(); - if (empty($content['username']) || empty($content['password'])) { + if (empty($body['username']) || empty($body['password'])) { print $this->jsonResponse->create(400, 'Please provide an username and password.'); exit(); } - $user = $this->userRepository->findOneByUsername($content['username']); + $user = $this->userRepository->findOneByUsername($body['username']); - if (!$this->security->passwordVerify($content['password'], $user['password'])) { + if (is_null($user) || !$this->security->passwordVerify($body['password'], $user['password'])) { print $this->jsonResponse->create(403, 'Bad credentials.'); exit(); } - $token = $this->security->generateToken(); + $token = $this->security->generateToken($user['id']); $expire_at = new \DateTime(); - $expire_at->add(new \DateInterval('P1D')); // Expire in 1 day + $expire_at->modify('+1 Day'); // Expire in 1 day - $stmt = $this->db->getConnection()->prepare('INSERT INTO Session (`user_id`, `token`, `issued_at`, `expire_at`) VALUES(:user_id, :token, NOW(), :expire_at)'); - $stmt->bindParam(':user_id', $user['id']); - $stmt->bindParam(':token', $token); - $stmt->bindParam(':expire_at', date_format($expire_at, 'Y-m-d')); - $stmt->execute(); + $this->sessionRepository->create($user['id'], $token, $expire_at->format('Y-m-d H:i:s')); print $this->jsonResponse->create(200, 'Welcome ' . $user['name'], [ - 'jwt_token' => $token, + 'token' => $token, 'expire_at' => $expire_at, ]); } - /** + /**db * Register route */ public function signup() { - $content = json_decode(trim(file_get_contents("php://input")), true); + $body = $this->request->getContent()->jsonToArray(); - if (empty($content['username']) || empty($content['email']) || empty($content['password'])) { + if (empty($body['username']) || empty($body['email']) || empty($body['password'])) { print $this->jsonResponse->create(400, 'Please provide an username, email and password.'); exit(); } $user = [ - 'username' => $content['username'], - 'email' => $content['email'], - 'password' => $this->security->passwordHash($content['password']), + 'username' => $body['username'], + 'email' => $body['email'], + 'password' => $this->security->passwordHash($body['password']), ]; - $stmt = $this->db->getConnection()->prepare('INSERT INTO User (`name`, `email`, `password`) VALUES(:name, :email, :password)'); - $stmt->bindParam(':name', $user['username']); - $stmt->bindParam(':email', $user['email']); - $stmt->bindParam(':password', $user['password']); - $stmt->execute(); + if (!is_null($this->userRepository->findOneByEmail($user['email']))) { + print $this->jsonResponse->create(403, 'Email already registered!'); + exit(); + } - print $this->jsonResponse->create(200, 'Success! Now send your credentials to /auth to sign in.', [ + $this->userRepository->create($user['username'], $user['email'], $user['password']); + + print $this->jsonResponse->create(200, 'Success. Now send your credentials to /auth to sign in.', [ 'username' => $user['username'], 'email' => $user['email'], ]); } + /** + * Signout + */ public function signout() { - // logout + if (!$this->security->isLogged()) { + print $this->security->NotAllowedRequest(); + exit(); + } + + $this->sessionRepository->deleteByToken($this->security->getBearerToken()); + + print $this->jsonResponse->create(200, 'Good bye.', []); + } + + /** + * Whois route + */ + public function me() + { + if (!$this->security->isLogged()) { + print $this->security->NotAllowedRequest(); + exit(); + } + + print $this->jsonResponse->create(200, 'hello!', $this->session->getUser()); } } \ No newline at end of file diff --git a/server/src/Repository/SessionRepository.php b/server/src/Repository/SessionRepository.php index e69de29..a01a4b2 100644 --- a/server/src/Repository/SessionRepository.php +++ b/server/src/Repository/SessionRepository.php @@ -0,0 +1,142 @@ +db = $db; + $this->security = $security; + $this->tableName = 'Session'; + } + + /** + * @return mixed + */ + public function findAll() + { + $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' ORDER BY id DESC'); + $stmt->execute(); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + /** + * @param $id + * @return null + */ + public function findOneById($id) + { + $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' WHERE id = :id'); + $stmt->bindParam(':id', $id, \PDO::PARAM_INT); + $stmt->execute(); + + $task = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$task) { + return null; + } else { + return $task; + } + } + + public function findUserBySessionToken($token) + { + $stmt = $this->db->getConnection()->prepare('SELECT s.token, s.expire_at, u.id, u.name, u.email FROM ' . $this->tableName . ' AS s INNER JOIN User as u ON s.user_id = u.id WHERE s.token = :token'); + $stmt->bindParam(':token', $token, \PDO::PARAM_STR); + $stmt->execute(); + + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + + /** + * @param $user_id + * @param $csrf + * @param $cookie + */ + public function create($user_id, $token, $expiration) + { + $stmt = $this->db->getConnection()->prepare('INSERT INTO Session (`user_id`, `token`, `issued_at`, `expire_at`) VALUES(:user_id, :token, NOW(), :expire_at)'); + $stmt->bindParam(':user_id', $user_id); + $stmt->bindParam(':token', $token); + $stmt->bindParam(':expire_at', $expiration); + $stmt->execute(); + } + + /** + * @param $id + * @param $data + * @return mixed + */ + public function updateById($id, $data) + { + $session = $this->findOneById($id); + + /*$stmt = $this->db->getConnection()->prepare('UPDATE ' . $this->tableName . ' SET user_id = :user_id, title = :title, description = :description, creation_date = :creation_date, status = :status'); + $stmt->bindParam(':user_id', $data['user_id'] ?? $task['user_id'], \PDO::PARAM_INT); + $stmt->bindParam(':title', $data['title'] ?? $task['title'], \PDO::PARAM_STR); + $stmt->bindParam(':description', $data['description'] ?? $task['description'], \PDO::PARAM_STR); + $stmt->bindParam(':creation_date', $data['creation_date'] ?? $task['creation_date']); + $stmt->bindParam(':status', $data['status'] ?? $task['status'], \PDO::PARAM_INT); + $stmt->execute(); + + return $data;*/ + } + + /** + * @param $id + */ + public function deleteById($id) + { + $stmt = $this->db->getConnection()->prepare('DELETE FROM ' . $this->tableName . ' WHERE id = :id'); + $stmt->bindParam(':id', $id, \PDO::PARAM_INT); + $stmt->execute(); + } + + /** + * @param $token + */ + public function deleteByToken($token) + { + $stmt = $this->db->getConnection()->prepare('DELETE FROM ' . $this->tableName . ' WHERE token = :token'); + $stmt->bindParam(':token', $token, \PDO::PARAM_STR); + $stmt->execute(); + } + + /** + * @param $userId + */ + public function deleteByUserId($userId) + { + $stmt = $this->db->getConnection()->prepare('DELETE FROM ' . $this->tableName . ' WHERE user_id = :user_id'); + $stmt->bindParam(':user_id', $userId, \PDO::PARAM_INT); + $stmt->execute(); + } +} \ No newline at end of file diff --git a/server/src/Service/Session.php b/server/src/Service/Session.php index 090efb6..5cc09d5 100644 --- a/server/src/Service/Session.php +++ b/server/src/Service/Session.php @@ -2,6 +2,8 @@ namespace App\Service; +use App\Repository\SessionRepository; + /** * Class Session * @package App\Service @@ -9,6 +11,7 @@ namespace App\Service; class Session { private $db; + private $sessionRepository; public $security; /** @@ -18,23 +21,7 @@ class Session { $this->db = $database; $this->security = new Security($this, $jsonResponse); - } - - /** - * @param $user_id - * @param $csrf - * @param $cookie - */ - public function create($user_id) - { - $token = $this->security->generateToken(); - $expire_at = new \DateTime(); - - $stmt = $this->db->getConnection()->prepare('INSERT INTO Session (user_id, token, issued_at, expire_at) VALUES(:user_id, :token, NOW(), :expire_at)'); - $stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT); - $stmt->bindParam(':token', $token, \PDO::PARAM_STR); - $stmt->bindParam(':expire_at', $expire_at); - $stmt->execute(); + $this->sessionRepository = new SessionRepository($this->db, $this->security); } /** @@ -55,4 +42,12 @@ class Session return $session; } } + + /** + * @return array + */ + public function getUser() + { + return $this->sessionRepository->findUserBySessionToken($this->security->getBearerToken()); + } } \ No newline at end of file From c92fd5f28fa637a8c76d17152075e4642c79fe2c Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 26 Jul 2018 15:50:12 +0200 Subject: [PATCH 28/29] User authentication and register --- server/src/Controller/TaskController.php | 40 +++++++++++++++---- server/src/Repository/UserRepository.php | 37 ++++++++++++++++++ server/src/Service/Security.php | 50 ++++++++++++++++++++++-- 3 files changed, 116 insertions(+), 11 deletions(-) diff --git a/server/src/Controller/TaskController.php b/server/src/Controller/TaskController.php index 644df41..a75e978 100644 --- a/server/src/Controller/TaskController.php +++ b/server/src/Controller/TaskController.php @@ -74,9 +74,9 @@ class TaskController exit(); } - $content = $this->request->getContentAsArray(); + $body = $this->request->getContent()->jsonToArray(); - if (empty($content['title']) || empty($content['description'])) { + if (empty($body['title']) || empty($body['description'])) { $code = 400; $message = 'Bad parameters.'; @@ -84,10 +84,12 @@ class TaskController exit(); } + $user = $this->session->getUser(); + $task = $this->repository->create([ - 'user_id' => 1, - 'title' => $content['title'], - 'description' => $content['description'], + 'user_id' => $user['id'], + 'title' => $body['title'], + 'description' => $body['description'], 'status' => 1 ]); @@ -106,9 +108,30 @@ class TaskController */ public function put($id) { + if (!$this->security->isLogged()) { + print $this->security->NotAllowedRequest(); + exit(); + } + + $task = $this->repository->findOneById($id); + $user = $this->session->getUser(); + + if ($task['user_id'] !== $user['id']) { + print $this->security->NotAllowedRequest(); + exit(); + } + + $body = $this->request->getContent()->jsonToArray(); + + $task = $this->repository->updateById($id, [ + 'title' => $body['title'] ?? $task['title'], + 'description' => $body['description'] ?? $task['description'], + 'status' => $body['status'] ?? $task['status'] + ]); + $code = 200; - $message = ""; - $data = []; + $message = "Task edited."; + $data = $task; print $this->jsonResponse->create($code, $message, $data); } @@ -127,8 +150,9 @@ class TaskController } $task = $this->repository->findOneById($id); + $user = $this->session->getUser(); - if ($task['user_id'] !== 1) { + if ($task['user_id'] !== $user['id']) { print $this->security->NotAllowedRequest(); exit(); } diff --git a/server/src/Repository/UserRepository.php b/server/src/Repository/UserRepository.php index 09f2d87..23c53fa 100644 --- a/server/src/Repository/UserRepository.php +++ b/server/src/Repository/UserRepository.php @@ -57,6 +57,10 @@ class UserRepository } } + /** + * @param $username + * @return null + */ public function findOneByUsername($username) { $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' WHERE name = :username'); @@ -71,4 +75,37 @@ class UserRepository return $user; } } + + /** + * @param $email + * @return null + */ + public function findOneByEmail($email) + { + $stmt = $this->db->getConnection()->prepare('SELECT * FROM ' . $this->tableName . ' WHERE email = :email'); + $stmt->bindParam(':email', $email, \PDO::PARAM_INT); + $stmt->execute(); + + $user = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$user) { + return null; + } else { + return $user; + } + } + + /** + * @param $username + * @param $email + * @param $password + */ + public function create($username, $email, $password) + { + $stmt = $this->db->getConnection()->prepare('INSERT INTO ' . $this->tableName . ' (`name`, `email`, `password`) VALUES(:name, :email, :password)'); + $stmt->bindParam(':name', $username); + $stmt->bindParam(':email', $email); + $stmt->bindParam(':password', $password); + $stmt->execute(); + } } \ No newline at end of file diff --git a/server/src/Service/Security.php b/server/src/Service/Security.php index b019321..1c0215c 100644 --- a/server/src/Service/Security.php +++ b/server/src/Service/Security.php @@ -36,9 +36,9 @@ class Security /** * @return string */ - public function generateToken() + public function generateToken($id) { - $token = md5(uniqid(rand(), TRUE)); + $token = md5($id . uniqid(rand(), TRUE)); return $token; } @@ -49,7 +49,14 @@ class Security */ public function isLogged() { - return false; + $session = $this->session->getSession($this->getBearerToken()); + $today = date("Y-m-d H:i:s"); + + if (is_null($session) || $session['expire_at'] < $today) { + return false; + } else { + return true; + } } /** @@ -84,4 +91,41 @@ class Security { return \password_verify($password, $hash); } + + /** + * Get hearder Authorization + * */ + function getAuthorizationHeader() + { + $headers = null; + if (isset($_SERVER['Authorization'])) { + $headers = trim($_SERVER["Authorization"]); + } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI + $headers = trim($_SERVER["HTTP_AUTHORIZATION"]); + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = apache_request_headers(); + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + //print_r($requestHeaders); + if (isset($requestHeaders['Authorization'])) { + $headers = trim($requestHeaders['Authorization']); + } + } + return $headers; + } + + /** + * Get access token from header + */ + function getBearerToken() + { + $headers = $this->getAuthorizationHeader(); + // HEADER: Get the access token from the header + if (!empty($headers)) { + if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { + return $matches[1]; + } + } + return null; + } } \ No newline at end of file From 97088a61421b218ea90da4f62d68befb90abf3cf Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 26 Jul 2018 15:51:09 +0200 Subject: [PATCH 29/29] [Fix #4] Tasks API endpoints --- server/src/Repository/TaskRepository.php | 13 +++++-------- server/src/Service/Request.php | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/server/src/Repository/TaskRepository.php b/server/src/Repository/TaskRepository.php index 392d39c..0d138ef 100644 --- a/server/src/Repository/TaskRepository.php +++ b/server/src/Repository/TaskRepository.php @@ -81,14 +81,11 @@ class TaskRepository */ public function updateById($id, $data) { - $task = $this->findOneById($id); - - $stmt = $this->db->getConnection()->prepare('UPDATE ' . $this->tableName . ' SET user_id = :user_id, title = :title, description = :description, creation_date = :creation_date, status = :status'); - $stmt->bindParam(':user_id', $data['user_id'] ?? $task['user_id'], \PDO::PARAM_INT); - $stmt->bindParam(':title', $data['title'] ?? $task['title'], \PDO::PARAM_STR); - $stmt->bindParam(':description', $data['description'] ?? $task['description'], \PDO::PARAM_STR); - $stmt->bindParam(':creation_date', $data['creation_date'] ?? $task['creation_date']); - $stmt->bindParam(':status', $data['status'] ?? $task['status'], \PDO::PARAM_INT); + $stmt = $this->db->getConnection()->prepare('UPDATE ' . $this->tableName . ' SET title = :title, description = :description, status = :status WHERE id = :id'); + $stmt->bindParam(':id', $id, \PDO::PARAM_INT); + $stmt->bindParam(':title', $data['title'], \PDO::PARAM_STR); + $stmt->bindParam(':description', $data['description'], \PDO::PARAM_STR); + $stmt->bindParam(':status', $data['status'], \PDO::PARAM_INT); $stmt->execute(); return $data; diff --git a/server/src/Service/Request.php b/server/src/Service/Request.php index 5053286..ee4ca32 100644 --- a/server/src/Service/Request.php +++ b/server/src/Service/Request.php @@ -8,8 +8,20 @@ namespace App\Service; */ class Request { - public function getContentAsArray() + private $content; + + public function getContent() { - return $content = json_decode(trim(file_get_contents("php://input")), true) ?? []; + $this->content = trim(file_get_contents("php://input")) ?? []; + + return $this; + } + + public function asPlainText(){ + return (string) $this->content; + } + + public function jsonToArray(){ + return json_decode($this->content, true); } } \ No newline at end of file