From b8f7532e5c7e8b8be3ab199fca3dadd0d22be4cd Mon Sep 17 00:00:00 2001
From: Frederic Guillot <fred@kanboard.net>
Date: Sat, 3 Dec 2016 15:43:36 -0500
Subject: Add personal API access token

---
 app/Api/Middleware/AuthenticationMiddleware.php |   1 +
 app/Auth/ApiAccessTokenAuth.php                 | 119 ++++++++++++++++++++++++
 app/Auth/DatabaseAuth.php                       |   2 +-
 app/Auth/LdapAuth.php                           |   2 +-
 app/Auth/RememberMeAuth.php                     |   4 +-
 app/Auth/ReverseProxyAuth.php                   |   2 +-
 app/Auth/TotpAuth.php                           |   2 +-
 app/Controller/UserApiAccessController.php      |  50 ++++++++++
 app/Core/Session/SessionStorage.php             |   1 +
 app/Locale/bs_BA/translations.php               |   9 ++
 app/Locale/cs_CZ/translations.php               |   9 ++
 app/Locale/da_DK/translations.php               |   9 ++
 app/Locale/de_DE/translations.php               |   9 ++
 app/Locale/el_GR/translations.php               |   9 ++
 app/Locale/es_ES/translations.php               |   9 ++
 app/Locale/fi_FI/translations.php               |   9 ++
 app/Locale/fr_FR/translations.php               |   9 ++
 app/Locale/hu_HU/translations.php               |   9 ++
 app/Locale/id_ID/translations.php               |   9 ++
 app/Locale/it_IT/translations.php               |   9 ++
 app/Locale/ja_JP/translations.php               |   9 ++
 app/Locale/ko_KR/translations.php               |   9 ++
 app/Locale/my_MY/translations.php               |   9 ++
 app/Locale/nb_NO/translations.php               |   9 ++
 app/Locale/nl_NL/translations.php               |   9 ++
 app/Locale/pl_PL/translations.php               |   9 ++
 app/Locale/pt_BR/translations.php               |   9 ++
 app/Locale/pt_PT/translations.php               |   9 ++
 app/Locale/ru_RU/translations.php               |   9 ++
 app/Locale/sr_Latn_RS/translations.php          |   9 ++
 app/Locale/sv_SE/translations.php               |   9 ++
 app/Locale/th_TH/translations.php               |   9 ++
 app/Locale/tr_TR/translations.php               |   9 ++
 app/Locale/zh_CN/translations.php               |   9 ++
 app/Schema/Mysql.php                            |   7 +-
 app/Schema/Postgres.php                         |   7 +-
 app/Schema/Sql/mysql.sql                        |   5 +-
 app/Schema/Sql/postgres.sql                     |   9 +-
 app/Schema/Sqlite.php                           |   7 +-
 app/ServiceProvider/AuthenticationProvider.php  |   3 +
 app/ServiceProvider/RouteProvider.php           |   1 +
 app/Template/user_api_access/show.php           |  17 ++++
 app/Template/user_view/sidebar.php              |   5 +
 43 files changed, 454 insertions(+), 15 deletions(-)
 create mode 100644 app/Auth/ApiAccessTokenAuth.php
 create mode 100644 app/Controller/UserApiAccessController.php
 create mode 100644 app/Template/user_api_access/show.php

(limited to 'app')

diff --git a/app/Api/Middleware/AuthenticationMiddleware.php b/app/Api/Middleware/AuthenticationMiddleware.php
index 8e309593..c4fa874a 100644
--- a/app/Api/Middleware/AuthenticationMiddleware.php
+++ b/app/Api/Middleware/AuthenticationMiddleware.php
@@ -28,6 +28,7 @@ class AuthenticationMiddleware extends Base implements MiddlewareInterface
     public function execute($username, $password, $procedureName)
     {
         $this->dispatcher->dispatch('app.bootstrap');
+        $this->sessionStorage->scope = 'API';
 
         if ($this->isUserAuthenticated($username, $password)) {
             $this->userSession->initialize($this->userModel->getByUsername($username));
diff --git a/app/Auth/ApiAccessTokenAuth.php b/app/Auth/ApiAccessTokenAuth.php
new file mode 100644
index 00000000..12ab21a7
--- /dev/null
+++ b/app/Auth/ApiAccessTokenAuth.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
+use Kanboard\Model\UserModel;
+use Kanboard\User\DatabaseUserProvider;
+
+/**
+ * API Access Token Authentication Provider
+ *
+ * @package  Kanboard\Auth
+ * @author   Frederic Guillot
+ */
+class ApiAccessTokenAuth extends Base implements PasswordAuthenticationProviderInterface
+{
+    /**
+     * User properties
+     *
+     * @access protected
+     * @var array
+     */
+    protected $userInfo = array();
+
+    /**
+     * Username
+     *
+     * @access protected
+     * @var string
+     */
+    protected $username = '';
+
+    /**
+     * Password
+     *
+     * @access protected
+     * @var string
+     */
+    protected $password = '';
+
+    /**
+     * Get authentication provider name
+     *
+     * @access public
+     * @return string
+     */
+    public function getName()
+    {
+        return 'API Access Token';
+    }
+
+    /**
+     * Authenticate the user
+     *
+     * @access public
+     * @return boolean
+     */
+    public function authenticate()
+    {
+        if (! isset($this->sessionStorage->scope) ||  $this->sessionStorage->scope !== 'API') {
+            $this->logger->debug(__METHOD__.': Authentication provider skipped because invalid scope');
+            return false;
+        }
+
+        $user = $this->db
+            ->table(UserModel::TABLE)
+            ->columns('id', 'password')
+            ->eq('username', $this->username)
+            ->eq('api_access_token', $this->password)
+            ->notNull('api_access_token')
+            ->eq('is_active', 1)
+            ->findOne();
+
+        if (! empty($user)) {
+            $this->userInfo = $user;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get user object
+     *
+     * @access public
+     * @return \Kanboard\User\DatabaseUserProvider
+     */
+    public function getUser()
+    {
+        if (empty($this->userInfo)) {
+            return null;
+        }
+
+        return new DatabaseUserProvider($this->userInfo);
+    }
+
+    /**
+     * Set username
+     *
+     * @access public
+     * @param  string $username
+     */
+    public function setUsername($username)
+    {
+        $this->username = $username;
+    }
+
+    /**
+     * Set password
+     *
+     * @access public
+     * @param  string $password
+     */
+    public function setPassword($password)
+    {
+        $this->password = $password;
+    }
+}
diff --git a/app/Auth/DatabaseAuth.php b/app/Auth/DatabaseAuth.php
index ecb42c17..84a1e019 100644
--- a/app/Auth/DatabaseAuth.php
+++ b/app/Auth/DatabaseAuth.php
@@ -11,7 +11,7 @@ use Kanboard\User\DatabaseUserProvider;
 /**
  * Database Authentication Provider
  *
- * @package  auth
+ * @package  Kanboard\Auth
  * @author   Frederic Guillot
  */
 class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterface, SessionCheckProviderInterface
diff --git a/app/Auth/LdapAuth.php b/app/Auth/LdapAuth.php
index a8dcfcb6..05ffbebf 100644
--- a/app/Auth/LdapAuth.php
+++ b/app/Auth/LdapAuth.php
@@ -12,7 +12,7 @@ use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
 /**
  * LDAP Authentication Provider
  *
- * @package  auth
+ * @package  Kanboard\Auth
  * @author   Frederic Guillot
  */
 class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
diff --git a/app/Auth/RememberMeAuth.php b/app/Auth/RememberMeAuth.php
index 5d0a8b2e..e0f4ceb6 100644
--- a/app/Auth/RememberMeAuth.php
+++ b/app/Auth/RememberMeAuth.php
@@ -7,9 +7,9 @@ use Kanboard\Core\Security\PreAuthenticationProviderInterface;
 use Kanboard\User\DatabaseUserProvider;
 
 /**
- * Rember Me Cookie Authentication Provider
+ * RememberMe Cookie Authentication Provider
  *
- * @package  auth
+ * @package  Kanboard\Auth
  * @author   Frederic Guillot
  */
 class RememberMeAuth extends Base implements PreAuthenticationProviderInterface
diff --git a/app/Auth/ReverseProxyAuth.php b/app/Auth/ReverseProxyAuth.php
index fdf936b1..02afc302 100644
--- a/app/Auth/ReverseProxyAuth.php
+++ b/app/Auth/ReverseProxyAuth.php
@@ -10,7 +10,7 @@ use Kanboard\User\ReverseProxyUserProvider;
 /**
  * Reverse-Proxy Authentication Provider
  *
- * @package  auth
+ * @package  Kanboard\Auth
  * @author   Frederic Guillot
  */
 class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterface, SessionCheckProviderInterface
diff --git a/app/Auth/TotpAuth.php b/app/Auth/TotpAuth.php
index 8e1ebe35..abfb2168 100644
--- a/app/Auth/TotpAuth.php
+++ b/app/Auth/TotpAuth.php
@@ -11,7 +11,7 @@ use Kanboard\Core\Security\PostAuthenticationProviderInterface;
 /**
  * TOTP Authentication Provider
  *
- * @package  auth
+ * @package  Kanboard\Auth
  * @author   Frederic Guillot
  */
 class TotpAuth extends Base implements PostAuthenticationProviderInterface
diff --git a/app/Controller/UserApiAccessController.php b/app/Controller/UserApiAccessController.php
new file mode 100644
index 00000000..e03514d5
--- /dev/null
+++ b/app/Controller/UserApiAccessController.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\Security\Token;
+
+/**
+ * Class UserApiAccessController
+ *
+ * @package Kanboard\Controller
+ * @author  Frederic Guillot
+ */
+class UserApiAccessController extends BaseController
+{
+    public function show()
+    {
+        $user = $this->getUser();
+
+        return $this->response->html($this->helper->layout->user('user_api_access/show', array(
+            'user'  => $user,
+            'title' => t('API User Access'),
+        )));
+    }
+
+    public function generate()
+    {
+        $user = $this->getUser();
+        $this->checkCSRFParam();
+
+        $this->userModel->update(array(
+            'id' => $user['id'],
+            'api_access_token' => Token::getToken(),
+        ));
+
+        $this->response->redirect($this->helper->url->to('UserApiAccessController', 'show', array('user_id' => $user['id'])));
+    }
+
+    public function remove()
+    {
+        $user = $this->getUser();
+        $this->checkCSRFParam();
+
+        $this->userModel->update(array(
+            'id' => $user['id'],
+            'api_access_token' => null,
+        ));
+
+        $this->response->redirect($this->helper->url->to('UserApiAccessController', 'show', array('user_id' => $user['id'])));
+    }
+}
\ No newline at end of file
diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php
index 9e93602c..e6478d8d 100644
--- a/app/Core/Session/SessionStorage.php
+++ b/app/Core/Session/SessionStorage.php
@@ -19,6 +19,7 @@ namespace Kanboard\Core\Session;
  * @property bool   $hasSubtaskInProgress
  * @property bool   $hasRememberMe
  * @property bool   $boardCollapsed
+ * @property string $scope
  * @property bool   $twoFactorBeforeCodeCalled
  * @property string $twoFactorSecret
  * @property string $oauthState
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index cb9ad9d5..e27a4501 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index 8541f303..5091de00 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 78a6d35e..2ca864e5 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 433b7a2f..950a3b77 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => 'Verschieben einer Aufgabe ist nicht erlaubt',
     'This value must be in the range %d to %d' => 'Dieser Wert muss im Bereich %d bis %d sein',
     'You are not allowed to move this task.' => 'Sie haben nicht die Berechtigung, diese Aufgabe zu verschieben.',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
index ad17b4ed..7b7d855e 100644
--- a/app/Locale/el_GR/translations.php
+++ b/app/Locale/el_GR/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index b3a68751..984d42db 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 733625b6..41e51fa1 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index 59f4609e..daac98ed 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -1279,4 +1279,13 @@ return array(
     'Moving a task is not permitted' => 'Déplaçer une tâche n\'est pas autorisé',
     'This value must be in the range %d to %d' => 'Cette valeur doit être définie entre %d et %d',
     'You are not allowed to move this task.' => 'Vous n\'êtes pas autorisé à déplacer cette tâche.',
+    'API User Access' => 'Accès utilisateur de l\'API',
+    'Preview' => 'Aperçu',
+    'Write' => 'Écrire',
+    'Write your text in Markdown' => 'Écrivez votre texte en Markdown',
+    'New External Task: %s' => 'Nouvelle tâche externe : %s',
+    'No personal API access token registered.' => 'Aucun jeton d\'accès personnel à l\'API enregistré.',
+    'Your personal API access token is "%s"' => 'Votre jeton d\'accès personnel à l\'API est « %s »',
+    'Remove your token' => 'Supprimer votre jeton',
+    'Generate a new token' => 'Générer un nouveau jeton',
 );
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index b5a23428..784c27ba 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index e711a512..9398d51d 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 3e27120b..65defe86 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => 'Spostare task non è permesso',
     'This value must be in the range %d to %d' => 'Questo valore deve essere compreso tra %d e %d',
     'You are not allowed to move this task.' => 'Non ti è permesso spostare questo task.',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index b9feed07..d0487fb7 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php
index 8c882b8a..a613b467 100644
--- a/app/Locale/ko_KR/translations.php
+++ b/app/Locale/ko_KR/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => '할일 이동이 거부되었습니다.',
     'This value must be in the range %d to %d' => '값의 범위는 %d 부터 %d 까지 입니다.',
     'You are not allowed to move this task.' => '당신은 할일 이동이 거부되었습니다.',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
index 048d8b4b..be335dec 100644
--- a/app/Locale/my_MY/translations.php
+++ b/app/Locale/my_MY/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 0d2a340d..6b49545c 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index e1745c14..44434a4e 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index 164abe0c..51c0fef8 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 637de30e..c0fa2387 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => 'Mover uma tarefa não é permitido',
     'This value must be in the range %d to %d' => 'Este valor precisa estar no intervalo %d até %d',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 3b8975d9..47763d30 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => 'Não é permitido mover uma tarefa',
     'This value must be in the range %d to %d' => 'Este valor deve estar entre %d e %d',
     'You are not allowed to move this task.' => 'Não lhe é permitido mover esta tarefa.',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 1e5c02c2..4d821f6a 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => 'Перемещение задачи не разрешено',
     'This value must be in the range %d to %d' => 'Значение должно находиться в диапазоне от %d до %d',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index 2f061260..470e3390 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index f32728e7..2ec4fa82 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 4025714a..5e0912fc 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -1278,4 +1278,13 @@ return array(
     // 'Moving a task is not permitted' => '',
     // 'This value must be in the range %d to %d' => '',
     // 'You are not allowed to move this task.' => '',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 24399891..1a648bbf 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => 'Görev taşımaya izin verilmemiş',
     'This value must be in the range %d to %d' => 'Bu değer şu aralıkta olmalı: "%d" "%d"',
     'You are not allowed to move this task.' => 'Bu görevi taşımaya izniniz yok.',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index e27a8a56..c87adbec 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -1278,4 +1278,13 @@ return array(
     'Moving a task is not permitted' => '禁止移动任务',
     'This value must be in the range %d to %d' => '输入值必须在%d到%d之间',
     'You are not allowed to move this task.' => '你不能移动此任务',
+    // 'API User Access' => '',
+    // 'Preview' => '',
+    // 'Write' => '',
+    // 'Write your text in Markdown' => '',
+    // 'New External Task: %s' => '',
+    // 'No personal API access token registered.' => '',
+    // 'Your personal API access token is "%s"' => '',
+    // 'Remove your token' => '',
+    // 'Generate a new token' => '',
 );
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 17285b12..fac8688a 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,12 @@ use PDO;
 use Kanboard\Core\Security\Token;
 use Kanboard\Core\Security\Role;
 
-const VERSION = 117;
+const VERSION = 118;
+
+function version_118(PDO $pdo)
+{
+    $pdo->exec('ALTER TABLE `users` ADD COLUMN `api_access_token` VARCHAR(255) DEFAULT NULL');
+}
 
 function version_117(PDO $pdo)
 {
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 082c31eb..32a7a744 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,12 @@ use PDO;
 use Kanboard\Core\Security\Token;
 use Kanboard\Core\Security\Role;
 
-const VERSION = 96;
+const VERSION = 97;
+
+function version_97(PDO $pdo)
+{
+    $pdo->exec('ALTER TABLE "users" ADD COLUMN api_access_token VARCHAR(255) DEFAULT NULL');
+}
 
 function version_96(PDO $pdo)
 {
diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql
index b39330e2..0ee88d88 100644
--- a/app/Schema/Sql/mysql.sql
+++ b/app/Schema/Sql/mysql.sql
@@ -715,6 +715,7 @@ CREATE TABLE `users` (
   `role` varchar(25) NOT NULL DEFAULT 'app-user',
   `is_active` tinyint(1) DEFAULT '1',
   `avatar_path` varchar(255) DEFAULT NULL,
+  `api_access_token` varchar(255) DEFAULT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `users_username_idx` (`username`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -738,7 +739,7 @@ CREATE TABLE `users` (
 
 LOCK TABLES `settings` WRITE;
 /*!40000 ALTER TABLE `settings` DISABLE KEYS */;
-INSERT INTO `settings` VALUES ('api_token','f9ae1d30899c88091642f4e996ff97b1e5db516b5edd256793246b18b637',0,0),('application_currency','USD',0,0),('application_datetime_format','m/d/Y H:i',1,1480728474),('application_date_format','m/d/Y',1,1480728474),('application_language','fr_FR',1,1480728474),('application_stylesheet','',1,1480728474),('application_timezone','UTC',1,1480728474),('application_time_format','H:i',1,1480728474),('application_url','',1,1480728474),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','checked',1,1480728474),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','382dae506e2bd5a4e45709c275827b0bbc7bee9f683b2320c78299deb36e',0,0),('webhook_url','',0,0);
+INSERT INTO `settings` VALUES ('api_token','f149956cb60c88d01123a28964fc035b1ce4513be454f2a85fe6b4ca3758',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','47d1d896b6612234c7543eb3f3a09a0a669f77a079d13ad3d810ccb79896',0,0),('webhook_url','',0,0);
 /*!40000 ALTER TABLE `settings` ENABLE KEYS */;
 UNLOCK TABLES;
 /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -767,4 +768,4 @@ UNLOCK TABLES;
 /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
 /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
 
-INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$ZYX2JNGPsH/SMc3UBk0rYu2LYCLYRVEqokhOjIULKXs6RjvaV2RBu', 'app-admin');INSERT INTO schema_version VALUES ('117');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$R1zYk04d96KcHRpd9.r5I.5I6mgKIgUdsaISZYmaDLPIJCUO0FFJG', 'app-admin');INSERT INTO schema_version VALUES ('118');
diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql
index 024c5cda..7a93b9f1 100644
--- a/app/Schema/Sql/postgres.sql
+++ b/app/Schema/Sql/postgres.sql
@@ -1245,7 +1245,8 @@ CREATE TABLE "users" (
     "gitlab_id" integer,
     "role" character varying(25) DEFAULT 'app-user'::character varying NOT NULL,
     "is_active" boolean DEFAULT true,
-    "avatar_path" character varying(255)
+    "avatar_path" character varying(255),
+    "api_access_token" character varying(255) DEFAULT NULL::character varying
 );
 
 
@@ -2544,8 +2545,8 @@ INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_high
 INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_public_refresh_interval', '60', 0, 0);
 INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_private_refresh_interval', '10', 0, 0);
 INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_columns', '', 0, 0);
-INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', 'c12181512f9cfd66a6b2af4edf199390b922bbb8a259dc5397c2329ed47c', 0, 0);
-INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', '4e05046a5cb5b907da712ab02af98e9752da48bfacc0a625c4c08493bc8f', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', '8687190194e06d34c2cd84a57b36f67696c971c2f8e453f96e59eccb8e73', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', '8381164131e3995ca17c754a5b0cf7039d66b9f389b80250978de9fcf2f5', 0, 0);
 INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_language', 'en_US', 0, 0);
 INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_timezone', 'UTC', 0, 0);
 INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_url', '', 0, 0);
@@ -2615,4 +2616,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true);
 -- PostgreSQL database dump complete
 --
 
-INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$ZYX2JNGPsH/SMc3UBk0rYu2LYCLYRVEqokhOjIULKXs6RjvaV2RBu', 'app-admin');INSERT INTO schema_version VALUES ('96');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$R1zYk04d96KcHRpd9.r5I.5I6mgKIgUdsaISZYmaDLPIJCUO0FFJG', 'app-admin');INSERT INTO schema_version VALUES ('97');
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index edf6ce63..11dcd537 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token;
 use Kanboard\Core\Security\Role;
 use PDO;
 
-const VERSION = 107;
+const VERSION = 108;
+
+function version_108(PDO $pdo)
+{
+    $pdo->exec('ALTER TABLE users ADD COLUMN api_access_token VARCHAR(255) DEFAULT NULL');
+}
 
 function version_107(PDO $pdo)
 {
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
index adff1e63..c2dad0e6 100644
--- a/app/ServiceProvider/AuthenticationProvider.php
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -2,6 +2,7 @@
 
 namespace Kanboard\ServiceProvider;
 
+use Kanboard\Auth\ApiAccessTokenAuth;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
 use Kanboard\Core\Security\AuthenticationManager;
@@ -44,6 +45,8 @@ class AuthenticationProvider implements ServiceProviderInterface
             $container['authenticationManager']->register(new LdapAuth($container));
         }
 
+        $container['authenticationManager']->register(new ApiAccessTokenAuth($container));
+
         $container['projectAccessMap'] = $this->getProjectAccessMap();
         $container['applicationAccessMap'] = $this->getApplicationAccessMap();
         $container['apiAccessMap'] = $this->getApiAccessMap();
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
index 0d1a7931..52687647 100644
--- a/app/ServiceProvider/RouteProvider.php
+++ b/app/ServiceProvider/RouteProvider.php
@@ -158,6 +158,7 @@ class RouteProvider implements ServiceProviderInterface
             $container['route']->addRoute('user/:user_id/authentication', 'UserCredentialController', 'changeAuthentication');
             $container['route']->addRoute('user/:user_id/2fa', 'TwoFactorController', 'index');
             $container['route']->addRoute('user/:user_id/avatar', 'AvatarFileController', 'show');
+            $container['route']->addRoute('user/:user_id/api', 'UserApiAccessController', 'show');
 
             // Groups
             $container['route']->addRoute('groups', 'GroupListController', 'index');
diff --git a/app/Template/user_api_access/show.php b/app/Template/user_api_access/show.php
new file mode 100644
index 00000000..3d58e0d5
--- /dev/null
+++ b/app/Template/user_api_access/show.php
@@ -0,0 +1,17 @@
+<div class="page-header">
+    <h2><?= t('API User Access') ?></h2>
+</div>
+
+<p class="alert">
+    <?php if (empty($user['api_access_token'])): ?>
+        <?= t('No personal API access token registered.') ?>
+    <?php else: ?>
+        <?= t('Your personal API access token is "%s"', $user['api_access_token']) ?>
+    <?php endif ?>
+</p>
+
+<?php if (! empty($user['api_access_token'])): ?>
+    <?= $this->url->link(t('Remove your token'), 'UserApiAccessController', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?>
+<?php endif ?>
+
+<?= $this->url->link(t('Generate a new token'), 'UserApiAccessController', 'generate', array('user_id' => $user['id']), true, 'btn btn-blue') ?>
diff --git a/app/Template/user_view/sidebar.php b/app/Template/user_view/sidebar.php
index a80daefa..ef494e42 100644
--- a/app/Template/user_view/sidebar.php
+++ b/app/Template/user_view/sidebar.php
@@ -90,6 +90,11 @@
                     <?= $this->url->link(t('Integrations'), 'UserViewController', 'integrations', array('user_id' => $user['id'])) ?>
                 </li>
             <?php endif ?>
+            <?php if ($this->user->hasAccess('UserApiAccessController', 'show')): ?>
+                <li <?= $this->app->checkMenuSelection('UserApiAccessController', 'show') ?>>
+                    <?= $this->url->link(t('API'), 'UserApiAccessController', 'show', array('user_id' => $user['id'])) ?>
+                </li>
+            <?php endif ?>
         <?php endif ?>
 
         <?php if ($this->user->hasAccess('UserCredentialController', 'changeAuthentication')): ?>
-- 
cgit v1.2.3