summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-07-29 17:42:48 -0400
committerFrederic Guillot <fred@kanboard.net>2015-07-29 17:42:48 -0400
commitf595fb2786d884dbaf7ec87d53cee920a0655f0e (patch)
tree0da808ef2f679affa51eff80e172787098c13731
parent2eeb58ae0321f584652714080649302c3f83a831 (diff)
Add first draft of the user api
-rw-r--r--app/Api/Action.php2
-rw-r--r--app/Api/App.php2
-rw-r--r--app/Api/Auth.php40
-rw-r--r--app/Api/Base.php104
-rw-r--r--app/Api/Board.php1
-rw-r--r--app/Api/Category.php2
-rw-r--r--app/Api/Comment.php2
-rw-r--r--app/Api/File.php2
-rw-r--r--app/Api/Link.php2
-rw-r--r--app/Api/Me.php55
-rw-r--r--app/Api/Project.php25
-rw-r--r--app/Api/ProjectPermission.php2
-rw-r--r--app/Api/Subtask.php2
-rw-r--r--app/Api/Swimlane.php2
-rw-r--r--app/Api/Task.php30
-rw-r--r--app/Api/TaskLink.php2
-rw-r--r--app/Api/User.php2
-rw-r--r--composer.json2
-rw-r--r--composer.lock13
-rw-r--r--docs/api-json-rpc.markdown343
-rw-r--r--jsonrpc.php4
-rw-r--r--tests/functionals/UserApiTest.php200
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&amp;action=show&amp;task_id=1&amp;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));
+ }
+}