diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-07-29 17:42:48 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-07-29 17:42:48 -0400 |
commit | f595fb2786d884dbaf7ec87d53cee920a0655f0e (patch) | |
tree | 0da808ef2f679affa51eff80e172787098c13731 | |
parent | 2eeb58ae0321f584652714080649302c3f83a831 (diff) |
Add first draft of the user api
-rw-r--r-- | app/Api/Action.php | 2 | ||||
-rw-r--r-- | app/Api/App.php | 2 | ||||
-rw-r--r-- | app/Api/Auth.php | 40 | ||||
-rw-r--r-- | app/Api/Base.php | 104 | ||||
-rw-r--r-- | app/Api/Board.php | 1 | ||||
-rw-r--r-- | app/Api/Category.php | 2 | ||||
-rw-r--r-- | app/Api/Comment.php | 2 | ||||
-rw-r--r-- | app/Api/File.php | 2 | ||||
-rw-r--r-- | app/Api/Link.php | 2 | ||||
-rw-r--r-- | app/Api/Me.php | 55 | ||||
-rw-r--r-- | app/Api/Project.php | 25 | ||||
-rw-r--r-- | app/Api/ProjectPermission.php | 2 | ||||
-rw-r--r-- | app/Api/Subtask.php | 2 | ||||
-rw-r--r-- | app/Api/Swimlane.php | 2 | ||||
-rw-r--r-- | app/Api/Task.php | 30 | ||||
-rw-r--r-- | app/Api/TaskLink.php | 2 | ||||
-rw-r--r-- | app/Api/User.php | 2 | ||||
-rw-r--r-- | composer.json | 2 | ||||
-rw-r--r-- | composer.lock | 13 | ||||
-rw-r--r-- | docs/api-json-rpc.markdown | 343 | ||||
-rw-r--r-- | jsonrpc.php | 4 | ||||
-rw-r--r-- | tests/functionals/UserApiTest.php | 200 |
22 files changed, 762 insertions, 77 deletions
diff --git a/app/Api/Action.php b/app/Api/Action.php index 6187b776..a67e915c 100644 --- a/app/Api/Action.php +++ b/app/Api/Action.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class Action extends Base +class Action extends \Core\Base { public function getAvailableActions() { diff --git a/app/Api/App.php b/app/Api/App.php index 2fc32e91..24da4dd9 100644 --- a/app/Api/App.php +++ b/app/Api/App.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class App extends Base +class App extends \Core\Base { public function getTimezone() { diff --git a/app/Api/Auth.php b/app/Api/Auth.php new file mode 100644 index 00000000..9d401746 --- /dev/null +++ b/app/Api/Auth.php @@ -0,0 +1,40 @@ +<?php + +namespace Api; + +use JsonRPC\AuthenticationFailure; +use Symfony\Component\EventDispatcher\Event; + +/** + * Base class + * + * @package api + * @author Frederic Guillot + */ +class Auth extends Base +{ + /** + * Check api credentials + * + * @access public + * @param string $username + * @param string $password + * @param string $class + * @param string $method + */ + public function checkCredentials($username, $password, $class, $method) + { + $this->container['dispatcher']->dispatch('api.bootstrap', new Event); + + if ($username !== 'jsonrpc' && $this->authentication->authenticate($username, $password)) { + $this->checkProcedurePermission(true, $method); + $this->userSession->refresh($this->user->getByUsername($username)); + } + else if ($username === 'jsonrpc' && $password === $this->config->get('api_token')) { + $this->checkProcedurePermission(false, $method); + } + else { + throw new AuthenticationFailure('Wrong credentials'); + } + } +} diff --git a/app/Api/Base.php b/app/Api/Base.php index 445b35be..723b60b7 100644 --- a/app/Api/Base.php +++ b/app/Api/Base.php @@ -3,7 +3,7 @@ namespace Api; use JsonRPC\AuthenticationFailure; -use Symfony\Component\EventDispatcher\Event; +use JsonRPC\AccessDeniedException; /** * Base class @@ -13,21 +13,97 @@ use Symfony\Component\EventDispatcher\Event; */ abstract class Base extends \Core\Base { - /** - * Check api credentials - * - * @access public - * @param string $username - * @param string $password - * @param string $class - * @param string $method - */ - public function authentication($username, $password, $class, $method) + private $user_allowed_procedures = array( + 'getMe', + 'getMyDashboard', + 'getMyActivityStream', + 'createMyPrivateProject', + 'getMyProjectsList', + ); + + private $both_allowed_procedures = array( + 'getTimezone', + 'getVersion', + 'getProjectById', + 'getTask', + 'getTaskByReference', + 'getAllTasks', + 'openTask', + 'closeTask', + 'moveTaskPosition', + 'createTask', + 'updateTask', + 'getBoard', + ); + + public function checkProcedurePermission($is_user, $procedure) + { + $is_both_procedure = in_array($procedure, $this->both_allowed_procedures); + $is_user_procedure = in_array($procedure, $this->user_allowed_procedures); + + if ($is_user && ! $is_both_procedure && ! $is_user_procedure) { + throw new AccessDeniedException('Permission denied'); + } + else if (! $is_user && ! $is_both_procedure && $is_user_procedure) { + throw new AccessDeniedException('Permission denied'); + } + } + + public function checkProjectPermission($project_id) { - $this->container['dispatcher']->dispatch('api.bootstrap', new Event); + if ($this->userSession->isLogged() && ! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + throw new AccessDeniedException('Permission denied'); + } + } + + public function checkTaskPermission($task_id) + { + if ($this->userSession->isLogged()) { + $this->checkProjectPermission($this->taskFinder->getProjectId($task_id)); + } + } - if (! ($username === 'jsonrpc' && $password === $this->config->get('api_token'))) { - throw new AuthenticationFailure('Wrong credentials'); + protected function formatTask($task) + { + if (! empty($task)) { + $task['url'] = $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); } + + return $task; + } + + protected function formatTasks($tasks) + { + if (! empty($tasks)) { + foreach ($tasks as &$task) { + $task = $this->formatTask($task); + } + } + + return $tasks; + } + + protected function formatProject($project) + { + if (! empty($project)) { + $project['url'] = array( + 'board' => $this->helper->url->to('board', 'show', array('project_id' => $project['id']), '', true), + 'calendar' => $this->helper->url->to('calendar', 'show', array('project_id' => $project['id']), '', true), + 'list' => $this->helper->url->to('listing', 'show', array('project_id' => $project['id']), '', true), + ); + } + + return $project; + } + + protected function formatProjects($projects) + { + if (! empty($projects)) { + foreach ($projects as &$project) { + $project = $this->formatProject($project); + } + } + + return $projects; } } diff --git a/app/Api/Board.php b/app/Api/Board.php index 163131b6..93b99cce 100644 --- a/app/Api/Board.php +++ b/app/Api/Board.php @@ -12,6 +12,7 @@ class Board extends Base { public function getBoard($project_id) { + $this->checkProjectPermission($project_id); return $this->board->getBoard($project_id); } diff --git a/app/Api/Category.php b/app/Api/Category.php index f457ddf1..ad3c5ef9 100644 --- a/app/Api/Category.php +++ b/app/Api/Category.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class Category extends Base +class Category extends \Core\Base { public function getCategory($category_id) { diff --git a/app/Api/Comment.php b/app/Api/Comment.php index 19b84383..e40968b9 100644 --- a/app/Api/Comment.php +++ b/app/Api/Comment.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class Comment extends Base +class Comment extends \Core\Base { public function getComment($comment_id) { diff --git a/app/Api/File.php b/app/Api/File.php index 5b82179c..97aa9d82 100644 --- a/app/Api/File.php +++ b/app/Api/File.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class File extends Base +class File extends \Core\Base { public function getFile($file_id) { diff --git a/app/Api/Link.php b/app/Api/Link.php index b9084784..d883013d 100644 --- a/app/Api/Link.php +++ b/app/Api/Link.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class Link extends Base +class Link extends \Core\Base { /** * Get a link by id diff --git a/app/Api/Me.php b/app/Api/Me.php new file mode 100644 index 00000000..29a8052a --- /dev/null +++ b/app/Api/Me.php @@ -0,0 +1,55 @@ +<?php + +namespace Api; + +use Model\Subtask as SubtaskModel; +use Model\Task as TaskModel; + +/** + * Me API controller + * + * @package api + * @author Frederic Guillot + */ +class Me extends Base +{ + public function getMe() + { + return $this->session['user']; + } + + public function getMyDashboard() + { + $user_id = $this->userSession->getId(); + $projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll(); + $tasks = $this->taskFinder->getUserQuery($user_id)->findAll(); + + return array( + 'projects' => $this->formatProjects($projects), + 'tasks' => $this->formatTasks($tasks), + 'subtasks' => $this->subtask->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(), + ); + } + + public function getMyActivityStream() + { + return $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()), 100); + } + + public function createMyPrivateProject($name, $description = null) + { + $values = array( + 'name' => $name, + 'description' => $description, + 'is_private' => 1, + ); + + list($valid,) = $this->project->validateCreation($values); + return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false; + } + + public function getMyProjectsList() + { + return $this->projectPermission->getMemberProjects($this->userSession->getId()); + } +} diff --git a/app/Api/Project.php b/app/Api/Project.php index 4e4e10b8..c3ae503c 100644 --- a/app/Api/Project.php +++ b/app/Api/Project.php @@ -12,6 +12,7 @@ class Project extends Base { public function getProjectById($project_id) { + $this->checkProjectPermission($project_id); return $this->formatProject($this->project->getById($project_id)); } @@ -82,28 +83,4 @@ class Project extends Base list($valid,) = $this->project->validateModification($values); return $valid && $this->project->update($values); } - - private function formatProject($project) - { - if (! empty($project)) { - $project['url'] = array( - 'board' => $this->helper->url->to('board', 'show', array('project_id' => $project['id']), '', true), - 'calendar' => $this->helper->url->to('calendar', 'show', array('project_id' => $project['id']), '', true), - 'list' => $this->helper->url->to('listing', 'show', array('project_id' => $project['id']), '', true), - ); - } - - return $project; - } - - private function formatProjects($projects) - { - if (! empty($projects)) { - foreach ($projects as &$project) { - $project = $this->formatProject($project); - } - } - - return $projects; - } } diff --git a/app/Api/ProjectPermission.php b/app/Api/ProjectPermission.php index a31faf3d..7dd2dec6 100644 --- a/app/Api/ProjectPermission.php +++ b/app/Api/ProjectPermission.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class ProjectPermission extends Base +class ProjectPermission extends \Core\Base { public function getMembers($project_id) { diff --git a/app/Api/Subtask.php b/app/Api/Subtask.php index 2e6c30f2..2b8e7a1f 100644 --- a/app/Api/Subtask.php +++ b/app/Api/Subtask.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class Subtask extends Base +class Subtask extends \Core\Base { public function getSubtask($subtask_id) { diff --git a/app/Api/Swimlane.php b/app/Api/Swimlane.php index 322b0805..fb40841f 100644 --- a/app/Api/Swimlane.php +++ b/app/Api/Swimlane.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class Swimlane extends Base +class Swimlane extends \Core\Base { public function getActiveSwimlanes($project_id) { diff --git a/app/Api/Task.php b/app/Api/Task.php index 3b8c1ec8..946a9e88 100644 --- a/app/Api/Task.php +++ b/app/Api/Task.php @@ -14,16 +14,19 @@ class Task extends Base { public function getTask($task_id) { + $this->checkTaskPermission($task_id); return $this->formatTask($this->taskFinder->getById($task_id)); } public function getTaskByReference($project_id, $reference) { + $this->checkProjectPermission($project_id); return $this->formatTask($this->taskFinder->getByReference($project_id, $reference)); } public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN) { + $this->checkProjectPermission($project_id); return $this->formatTasks($this->taskFinder->getAll($project_id, $status_id)); } @@ -34,11 +37,13 @@ class Task extends Base public function openTask($task_id) { + $this->checkTaskPermission($task_id); return $this->taskStatus->open($task_id); } public function closeTask($task_id) { + $this->checkTaskPermission($task_id); return $this->taskStatus->close($task_id); } @@ -49,6 +54,7 @@ class Task extends Base public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0) { + $this->checkProjectPermission($project_id); return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); } @@ -57,6 +63,8 @@ class Task extends Base $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, $recurrence_basedate = 0, $reference = '') { + $this->checkProjectPermission($project_id); + $values = array( 'title' => $title, 'project_id' => $project_id, @@ -87,6 +95,8 @@ class Task extends Base $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) { + $this->checkTaskPermission($id); + $values = array( 'id' => $id, 'title' => $title, @@ -115,24 +125,4 @@ class Task extends Base list($valid) = $this->taskValidator->validateApiModification($values); return $valid && $this->taskModification->update($values); } - - private function formatTask($task) - { - if (! empty($task)) { - $task['url'] = $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); - } - - return $task; - } - - private function formatTasks($tasks) - { - if (! empty($tasks)) { - foreach ($tasks as &$task) { - $task = $this->formatTask($task); - } - } - - return $tasks; - } } diff --git a/app/Api/TaskLink.php b/app/Api/TaskLink.php index c3e1a83c..6b23d051 100644 --- a/app/Api/TaskLink.php +++ b/app/Api/TaskLink.php @@ -8,7 +8,7 @@ namespace Api; * @package api * @author Frederic Guillot */ -class TaskLink extends Base +class TaskLink extends \Core\Base { /** * Get a task link diff --git a/app/Api/User.php b/app/Api/User.php index 0f0f4e59..7409a41b 100644 --- a/app/Api/User.php +++ b/app/Api/User.php @@ -10,7 +10,7 @@ use Auth\Ldap; * @package api * @author Frederic Guillot */ -class User extends Base +class User extends \Core\Base { public function getUser($user_id) { diff --git a/composer.json b/composer.json index ced788c6..a6f6135e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "eluceo/ical": "*", "erusev/parsedown" : "1.5.3", "fabiang/xmpp" : "0.6.1", - "fguillot/json-rpc" : "1.0.0", + "fguillot/json-rpc" : "dev-master", "fguillot/picodb" : "1.0.0", "fguillot/simpleLogger" : "0.0.2", "fguillot/simple-validator" : "0.0.3", diff --git a/composer.lock b/composer.lock index 28a44f01..612167c1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "305f839bfc9c4acb5d9357e1174c42da", + "hash": "1c0cc116db3d03c38df0f0efa59e9df7", "packages": [ { "name": "christian-riesen/base32", @@ -260,16 +260,16 @@ }, { "name": "fguillot/json-rpc", - "version": "v1.0.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/fguillot/JsonRPC.git", - "reference": "5a11f1414780a200f09b78d20ab72b5cee4faa95" + "reference": "050f046b1cae99210ae2fe64618831d3961f5bd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/5a11f1414780a200f09b78d20ab72b5cee4faa95", - "reference": "5a11f1414780a200f09b78d20ab72b5cee4faa95", + "url": "https://api.github.com/repos/fguillot/JsonRPC/zipball/050f046b1cae99210ae2fe64618831d3961f5bd9", + "reference": "050f046b1cae99210ae2fe64618831d3961f5bd9", "shasum": "" }, "require": { @@ -292,7 +292,7 @@ ], "description": "Simple Json-RPC client/server library that just works", "homepage": "https://github.com/fguillot/JsonRPC", - "time": "2015-07-01 19:50:31" + "time": "2015-07-29 20:56:20" }, { "name": "fguillot/picodb", @@ -757,6 +757,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { + "fguillot/json-rpc": 20, "swiftmailer/swiftmailer": 0, "symfony/console": 0 }, diff --git a/docs/api-json-rpc.markdown b/docs/api-json-rpc.markdown index 8a637412..827f4099 100644 --- a/docs/api-json-rpc.markdown +++ b/docs/api-json-rpc.markdown @@ -1,6 +1,34 @@ Json-RPC API ============ +User and application API +------------------------ + +There are two types of API access: + +### Application API + +- Access to the API with the user "jsonrpc" and the token available in settings +- Access to all procedures +- No permission checked +- There is no user session on the server +- Example of possible clients: tools to migrate/import data, create tasks from another system, etc... + +### User API + +- Access to the API with the user credentials (username and password) +- Access to a restricted set of procedures +- The project permissions are checked +- A user session is created on the server +- Example of possible clients: mobile/desktop application, command line utility, etc... + +Security +-------- + +- Always use HTTPS with a valid certificate +- If you make a mobile application, it's your job to store securely the user credentials on the device +- Two factor authentication is not yet available through the API + Protocol -------- @@ -20,12 +48,37 @@ Authentication The API credentials are available on the settings page. - API end-point: `https://YOUR_SERVER/jsonrpc.php` + +If you want to use the "application api": + - Username: `jsonrpc` - Password: API token on the settings page +Otherwise for the "user api", just use the real username/passsword. + The API use the [HTTP Basic Authentication Scheme described in the RFC2617](http://www.ietf.org/rfc/rfc2617.txt). If there is an authentication error, you will receive the HTTP status code `401 Not Authorized`. +### Authorized User API procedures + +- getMe +- getMyDashboard +- getMyActivityStream +- createMyPrivateProject +- getMyProjectsList +- getTimezone +- getVersion +- getProjectById +- getTask +- getTaskByReference +- getAllTasks +- openTask +- closeTask +- moveTaskPosition +- createTask +- updateTask +- getBoard + ### Custom HTTP header You can use an alternative HTTP header for the authentication if your server have a very specific configuration. @@ -3831,3 +3884,293 @@ Response example: "result": true } ``` + +### getMe + +- Purpose: **Get logged user session** +- Parameters: None +- Result on success: **user session data** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getMe", + "id": 1718627783 +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1718627783, + "result": { + "id": 2, + "username": "user", + "is_admin": false, + "is_ldap_user": false, + "name": "", + "email": "", + "google_id": null, + "github_id": null, + "notifications_enabled": "0", + "timezone": null, + "language": null, + "disable_login_form": "0", + "twofactor_activated": false, + "twofactor_secret": null, + "token": "", + "notifications_filter": "4" + } +} +``` + +### getMyDashboard + +- Purpose: **Get the dashboard of the logged user without pagination** +- Parameters: None +- Result on success: **Dashboard information** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getMyDashboard", + "id": 447898718 +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1563664593, + "result": { + "projects": [ + { + "id": "2", + "name": "my project", + "is_active": "1", + "token": "", + "last_modified": "1438205337", + "is_public": "0", + "is_private": "1", + "is_everybody_allowed": "0", + "default_swimlane": "Default swimlane", + "show_default_swimlane": "1", + "description": null, + "identifier": "", + "columns": [ + { + "id": "5", + "title": "Backlog", + "position": "1", + "project_id": "2", + "task_limit": "0", + "description": "", + "nb_tasks": 0 + }, + { + "id": "6", + "title": "Ready", + "position": "2", + "project_id": "2", + "task_limit": "0", + "description": "", + "nb_tasks": 0 + }, + { + "id": "7", + "title": "Work in progress", + "position": "3", + "project_id": "2", + "task_limit": "0", + "description": "", + "nb_tasks": 0 + }, + { + "id": "8", + "title": "Done", + "position": "4", + "project_id": "2", + "task_limit": "0", + "description": "", + "nb_tasks": 0 + } + ], + "url": { + "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=2", + "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=2", + "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=2" + } + } + ], + "tasks": [ + { + "id": "1", + "title": "new title", + "date_due": "0", + "date_creation": "1438205336", + "project_id": "2", + "color_id": "yellow", + "time_spent": "0", + "time_estimated": "0", + "project_name": "my project", + "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=2" + } + ], + "subtasks": [] + } +} +``` + +### getMyActivityStream + +- Purpose: **Get the last 100 events for the logged user** +- Parameters: None +- Result on success: **List of events** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getMyActivityStream", + "id": 1132562181 +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1132562181, + "result": [ + { + "id": "1", + "date_creation": "1438205054", + "event_name": "task.create", + "creator_id": "2", + "project_id": "2", + "task_id": "1", + "author_username": "user", + "author_name": "", + "email": "", + "task": { + "id": "1", + "reference": "", + "title": "my user title", + "description": "", + "date_creation": "1438205054", + "date_completed": null, + "date_modification": "1438205054", + "date_due": "0", + "date_started": null, + "time_estimated": "0", + "time_spent": "0", + "color_id": "yellow", + "project_id": "2", + "column_id": "5", + "owner_id": "0", + "creator_id": "2", + "position": "1", + "is_active": "1", + "score": "0", + "category_id": "0", + "swimlane_id": "0", + "date_moved": "1438205054", + "recurrence_status": "0", + "recurrence_trigger": "0", + "recurrence_factor": "0", + "recurrence_timeframe": "0", + "recurrence_basedate": "0", + "recurrence_parent": null, + "recurrence_child": null, + "category_name": null, + "swimlane_name": null, + "project_name": "my project", + "default_swimlane": "Default swimlane", + "column_title": "Backlog", + "assignee_username": null, + "assignee_name": null, + "creator_username": "user", + "creator_name": "" + }, + "changes": [], + "author": "user", + "event_title": "user created the task #1", + "event_content": "\n<p class=\"activity-title\">\n user created the task <a href=\"\/?controller=task&action=show&task_id=1&project_id=2\" class=\"\" title=\"\" >#1<\/a><\/p>\n<p class=\"activity-description\">\n <em>my user title<\/em>\n<\/p>" + } + ] +} +``` + +### createMyPrivateProject + +- Purpose: **Create a private project for the logged user** +- Parameters: + - **name** (string, required) + - **description** (string, optional) +- Result on success: **project_id** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "createMyPrivateProject", + "id": 1271580569, + "params": [ + "my project" + ] +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 1271580569, + "result": 2 +} +``` + +### getMyProjectsList + +- Purpose: **Get projects of the connected user** +- Parameters: None +- Result on success: **dictionary of project_id => project_name** +- Result on failure: **false** + +Request example: + +```json +{ + "jsonrpc": "2.0", + "method": "getMyProjectsList", + "id": 987834805 +} +``` + +Response example: + +```json +{ + "jsonrpc": "2.0", + "id": 987834805, + "result": { + "2": "my project" + } +} +``` diff --git a/jsonrpc.php b/jsonrpc.php index 6c4b9d3d..adeb4ac4 100644 --- a/jsonrpc.php +++ b/jsonrpc.php @@ -4,7 +4,9 @@ require __DIR__.'/app/common.php'; $server = new JsonRPC\Server; $server->setAuthenticationHeader(API_AUTHENTICATION_HEADER); -$server->before('authentication'); +$server->before(array(new Api\Auth($container), 'checkCredentials')); + +$server->attach(new Api\Me($container)); $server->attach(new Api\Action($container)); $server->attach(new Api\App($container)); $server->attach(new Api\Board($container)); diff --git a/tests/functionals/UserApiTest.php b/tests/functionals/UserApiTest.php new file mode 100644 index 00000000..6fe6dad6 --- /dev/null +++ b/tests/functionals/UserApiTest.php @@ -0,0 +1,200 @@ +<?php + +require_once __DIR__.'/../../vendor/autoload.php'; + +class UserApi extends PHPUnit_Framework_TestCase +{ + private $app = null; + private $admin = null; + private $user = null; + + public static function setUpBeforeClass() + { + if (DB_DRIVER === 'sqlite') { + @unlink(DB_FILENAME); + } + else if (DB_DRIVER === 'mysql') { + $pdo = new PDO('mysql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); + $pdo->exec('DROP DATABASE '.DB_NAME); + $pdo->exec('CREATE DATABASE '.DB_NAME); + $pdo = null; + } + else if (DB_DRIVER === 'postgres') { + $pdo = new PDO('pgsql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD); + $pdo->exec('DROP DATABASE '.DB_NAME); + $pdo->exec('CREATE DATABASE '.DB_NAME.' WITH OWNER '.DB_USERNAME); + $pdo = null; + } + + $service = new ServiceProvider\DatabaseProvider; + + $db = $service->getInstance(); + $db->table('settings')->eq('option', 'api_token')->update(array('value' => API_KEY)); + $db->closeConnection(); + } + + public function setUp() + { + $this->app = new JsonRPC\Client(API_URL); + $this->app->authentication('jsonrpc', API_KEY); + $this->app->debug = true; + + $this->admin = new JsonRPC\Client(API_URL); + $this->admin->authentication('admin', 'admin'); + $this->admin->debug = true; + + $this->user = new JsonRPC\Client(API_URL); + $this->user->authentication('user', 'password'); + $this->user->debug = true; + } + + public function testCreateProject() + { + $this->assertEquals(1, $this->app->createProject('team project')); + } + + public function testCreateUser() + { + $this->assertEquals(2, $this->app->createUser('user', 'password')); + } + + /** + * @expectedException JsonRPC\AccessDeniedException + */ + public function testNotAllowedAppProcedure() + { + $this->app->getMe(); + } + + /** + * @expectedException JsonRPC\AccessDeniedException + */ + public function testNotAllowedUserProcedure() + { + $this->user->getAllProjects(); + } + + /** + * @expectedException JsonRPC\AccessDeniedException + */ + public function testNotAllowedProjectForUser() + { + $this->user->getProjectById(1); + } + + public function testAllowedProjectForAdmin() + { + $this->assertNotEmpty($this->admin->getProjectById(1)); + } + + public function testGetTimezone() + { + $this->assertEquals('UTC', $this->user->getTimezone()); + } + + public function testGetVersion() + { + $this->assertEquals('master', $this->user->getVersion()); + } + + public function testGetMe() + { + $profile = $this->user->getMe(); + $this->assertNotEmpty($profile); + $this->assertEquals('user', $profile['username']); + } + + public function testCreateMyPrivateProject() + { + $this->assertEquals(2, $this->user->createMyPrivateProject('my project')); + } + + public function testGetMyProjectsList() + { + $projects = $this->user->getMyProjectsList(); + $this->assertNotEmpty($projects); + $this->assertArrayNotHasKey(1, $projects); + $this->assertArrayHasKey(2, $projects); + $this->assertEquals('my project', $projects[2]); + } + + public function testGetProjectById() + { + $project = $this->user->getProjectById(2); + $this->assertNotEmpty($project); + $this->assertEquals('my project', $project['name']); + $this->assertEquals(1, $project['is_private']); + } + + public function testCreateTask() + { + $this->assertEquals(1, $this->user->createTask('my user title', 2)); + $this->assertEquals(2, $this->admin->createTask('my admin title', 1)); + } + + public function testGetTask() + { + $task = $this->user->getTask(1); + $this->assertNotEmpty($task); + $this->assertEquals('my user title', $task['title']); + } + + /** + * @expectedException JsonRPC\AccessDeniedException + */ + public function testGetAdminTask() + { + $this->user->getTask(2); + } + + public function testGetMyActivityStream() + { + $activity = $this->user->getMyActivityStream(); + $this->assertNotEmpty($activity); + } + + public function testCloseTask() + { + $this->assertTrue($this->user->closeTask(1)); + } + + public function testOpenTask() + { + $this->assertTrue($this->user->openTask(1)); + } + + public function testMoveTaskPosition() + { + $this->assertTrue($this->user->moveTaskPosition(2, 1, 2, 1)); + } + + public function testUpdateTask() + { + $this->assertTrue($this->user->updateTask(array('id' => 1, 'title' => 'new title', 'reference' => 'test', 'owner_id' => 2))); + } + + public function testGetbyReference() + { + $task = $this->user->getTaskByReference(2, 'test'); + $this->assertNotEmpty($task); + $this->assertEquals('new title', $task['title']); + $this->assertEquals(2, $task['column_id']); + $this->assertEquals(1, $task['position']); + } + + public function testGetMyDashboard() + { + $dashboard = $this->user->getMyDashboard(); + $this->assertNotEmpty($dashboard); + $this->assertArrayHasKey('projects', $dashboard); + $this->assertArrayHasKey('tasks', $dashboard); + $this->assertArrayHasKey('subtasks', $dashboard); + $this->assertNotEmpty($dashboard['projects']); + $this->assertNotEmpty($dashboard['tasks']); + } + + public function testGetBoard() + { + $this->assertNotEmpty($this->user->getBoard(2)); + } +} |