From 9591c0a71a96e110f5485f46c6fe8b8373cdb671 Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Nov 21 2017 11:42:46 +0000
Subject: [PATCH 1/7] Add user API CRUD


Signed-off-by: danmoura17 <danmoura17@gmail.com>
Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>

---

diff --git a/alembic/versions/cecf8298b202_added_user_table.py b/alembic/versions/cecf8298b202_added_user_table.py
new file mode 100644
index 0000000..2a18487
--- /dev/null
+++ b/alembic/versions/cecf8298b202_added_user_table.py
@@ -0,0 +1,37 @@
+"""Added user table.
+
+Revision ID: cecf8298b202
+Revises: 9ee67bf38f1f
+Create Date: 2017-11-09 16:50:47.759236
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'cecf8298b202'
+down_revision = '9ee67bf38f1f'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    """TODO: Add an upgrade description."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('users',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.Unicode(length=255), nullable=False),
+    sa.Column('email', sa.String(length=255), nullable=False),
+    sa.Column('password_hash', sa.String(length=128), nullable=True),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('email')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    """TODO: Add a downgrade description."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('users')
+    # ### end Alembic commands ###
diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 4acdb24..2a1e9a9 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -1,12 +1,13 @@
 """kiskadee API."""
-from flask import Flask, jsonify
+from flask import Flask, jsonify, abort, make_response
 from flask import request
 from flask_cors import CORS
+from marshmallow.exceptions import ValidationError
 
 from kiskadee.database import Database
-from kiskadee.model import Package, Fetcher, Version, Analysis
+from kiskadee.model import Package, Fetcher, Version, Analysis, User
 from kiskadee.api.serializers import PackageSchema, FetcherSchema,\
-        AnalysisSchema
+        AnalysisSchema, UserSchema
 import json
 
 kiskadee = Flask(__name__)
@@ -64,6 +65,149 @@ def package_analysis(pkg_name, version):
         return jsonify({'analysis': results.data})
 
 
+@kiskadee.route('/users', methods=['GET'])
+def get_users():
+    """Get the list of users
+
+    GET /users
+
+    Possible status code:
+        - 200 Ok -> Users list
+    """
+    db_session = kiskadee_db_session()
+    users = db_session.query(User).all()
+    user_schema = UserSchema(many=True)
+    result = user_schema.dump(users)
+
+    return make_response(jsonify({'users': result.data}), 200)
+
+
+@kiskadee.route('/users', methods=['POST'])
+def create_user():
+    """Create a new user
+
+    POST /users
+
+    Possible status code:
+        - 201 Created -> User created
+        - 400 Bad Request -> Validation error
+        - 403 Forbidden -> User already exists
+    """
+    db_session = kiskadee_db_session()
+    data = request.get_json()
+
+    # Verify is user already exists
+    if data.get('email'):
+        user = db_session.query(User).filter_by(email=data.get('email')).first()
+
+        if user is not None:
+            return make_response(jsonify({'error': 'user already exists'}), 403)
+
+    # Try to create user
+    try:
+        user = UserSchema.create(**data)
+    except ValidationError as error:
+        return make_response(jsonify({
+            'error': 'Validation error',
+            'validations': error.args[0]
+        }), 400)
+
+    db_session.add(user)
+    db_session.commit()
+
+    user_schema = UserSchema()
+    result = user_schema.dump(user)
+
+    return make_response(jsonify({'user': result.data}), 201)
+
+
+@kiskadee.route('/users/<user_id>', methods=['GET'])
+def get_user_data(user_id):
+    """Get the user data
+
+    GET /users/:id
+
+    Possible status code:
+        - 200 Ok -> User data
+        - 404 Not Found -> User not found
+    """
+    db_session = kiskadee_db_session()
+    user = db_session.query(User).filter_by(id=user_id).first()
+
+    if user is None:
+        return make_response(jsonify({'error': 'user not found'}), 404)
+
+    user_schema = UserSchema()
+    result = user_schema.dump(user)
+
+    return make_response(jsonify({'user': result.data}), 200)
+
+
+@kiskadee.route('/users/<user_id>', methods=['PUT'])
+def update_user(user_id):
+    """Updates a user
+
+    PUT /users/:id
+
+    Possible status code:
+        - 200 Ok -> User updated
+        - 400 Bad Request -> Validation error
+        - 404 Not Found -> User not found
+    """
+    db_session = kiskadee_db_session()
+    user = db_session.query(User).filter_by(id=user_id).first()
+
+    if user is None:
+        return make_response(jsonify({'error': 'user not found'}), 404)
+
+    json_data = request.get_json()
+    user_data = UserSchema().dump(user).data
+    user_data.update(json_data)
+
+    validation = UserSchema().load(user_data)
+
+    if bool(validation.errors):
+        return make_response(jsonify({
+            'error': 'Validation error',
+            'validations': validation.errors
+        }), 400)
+
+    password = validation.data.get('validation')
+    if password is not None:
+        user.hash_password(password)
+        del validation.data['password']
+
+    for (key, value) in validation.data.items():
+        setattr(user, key, value)
+
+    db_session.commit()
+
+    result = UserSchema().dump(user)
+    return make_response(jsonify({'user': result.data}), 200)
+
+
+@kiskadee.route('/users/<user_id>', methods=['DELETE'])
+def delete_user(user_id):
+    """Deletes a user
+
+    DELETE /users/:id
+
+    Possible status code:
+        - 204 No Content -> User deleted
+        - 404 Not Found -> User not found
+    """
+    db_session = kiskadee_db_session()
+    user = db_session.query(User).filter_by(id=user_id).first()
+
+    if user is None:
+        return make_response(jsonify({'error': 'user not found'}), 404)
+
+    db_session.delete(user)
+    db_session.commit()
+
+    return make_response(jsonify({}), 204)
+
+
 def kiskadee_db_session():
     """Return a kiskadee database session."""
     return Database().session
diff --git a/kiskadee/api/serializers.py b/kiskadee/api/serializers.py
index 98e7835..5fc16f6 100644
--- a/kiskadee/api/serializers.py
+++ b/kiskadee/api/serializers.py
@@ -1,7 +1,7 @@
 """Provide objects to serialize the kiskadee models."""
 
-from marshmallow import Schema, fields
-from kiskadee.model import Package, Fetcher, Analysis, Version, Report
+from marshmallow import Schema, fields, validate, exceptions
+from kiskadee.model import Package, Fetcher, Analysis, Version, Report, User
 
 
 class ReportsSchema(Schema):
@@ -72,3 +72,51 @@ class PackageSchema(Schema):
         """Serialize a Package object."""
         print('MAKING OBJECT FROM', data)
         return Package(**data)
+
+
+class UserSchema(Schema):
+    """Provide a serializer to the User model."""
+
+    id = fields.Int(dump_only=True)
+    name = fields.Str(required=True, validate=validate.Length(min=4, max=255))
+    email = fields.Str(
+        required=True,
+        validate=[validate.Email(error='Not a valid email address'),
+                  validate.Length(min=4, max=255)])
+    password = fields.Str(load_only=True,
+                          validate=validate.Length(min=4, max=255))
+
+    def make_object(self, data):
+        """Serialize a User object."""
+        print('MAKING OBJECT FROM', data)
+        return User(**data)
+
+    @classmethod
+    def create(cls, **data):
+        """User factory that creates a User object without saving it.
+
+        If the given data has errors it raises an
+        marshmallow.exceptions.ValidationError, else create an User model
+        and return it widthout saving.
+
+        :data: User model attributes
+        """
+        validation = UserSchema().load(data)
+
+        if bool(validation.errors):
+            raise exceptions.ValidationError(validation.errors)
+
+        password = validation.data.get('password')
+        if password is not None:
+            del validation.data['password']
+
+        user = User(**validation.data)
+
+        if password is not None:
+            user.hash_password(str(password))
+        else:
+            raise exceptions.ValidationError({
+                'password': 'Missing data for required field.'
+            })
+
+        return user
\ No newline at end of file
diff --git a/kiskadee/model.py b/kiskadee/model.py
index 6aa3233..86f2365 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -2,7 +2,8 @@
 
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy import Column, Integer, UnicodeText, UniqueConstraint,\
-                       Sequence, Unicode, ForeignKey, orm, JSON
+                       Sequence, Unicode, ForeignKey, orm, JSON, String
+from passlib.apps import custom_app_context as pwd_context
 import kiskadee
 
 Base = declarative_base()
@@ -105,3 +106,25 @@ def create_analyzers(_session):
             new_analyzer.version = version
             _session.add(new_analyzer)
     _session.commit()
+
+
+class User(Base):
+    __tablename__ = 'users'
+    id = Column(Integer,
+                Sequence('users_id_seq', optional=True), primary_key=True)
+    name = Column(Unicode(255), nullable=False)
+    email = Column(String(255), nullable=False, unique=True)
+    password_hash = Column(String(128))
+
+    def hash_password(self, password):
+        """Takes a plain password as argument
+        and stores a hash of it with the user.
+        """
+        self.password_hash = pwd_context.hash(password)
+
+    def verify_password(self, password):
+        """Takes a plain password as argument and returns
+        True if the password is correct
+        False if not.
+        """
+        return pwd_context.verify(password, self.password_hash)
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
new file mode 100644
index 0000000..70e24c8
--- /dev/null
+++ b/kiskadee/tests/test_api_users.py
@@ -0,0 +1,318 @@
+import json
+import unittest
+from sqlalchemy.orm import sessionmaker
+from unittest.mock import patch
+
+import kiskadee
+import kiskadee.api.app
+
+from kiskadee.model import User
+from kiskadee.api.serializers import UserSchema
+
+def mock_hash_password(self, password):
+    """Mock for User model hash_password method.
+    It is too slow for the tests.
+    """
+    self.password_hash = str(password)
+
+
+class ApiUsersTestCase(unittest.TestCase):
+
+    def setUp(self):
+        kiskadee.api.app.kiskadee.testing = True
+        self.engine = kiskadee.database.Database('db_test').engine
+        Session = sessionmaker(bind=self.engine)
+        self.session = Session()
+        self.app = kiskadee.api.app.kiskadee.test_client()
+
+        kiskadee.model.Base.metadata.create_all(self.engine)
+        self._setup_mock_users()
+
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def _setup_mock_users(self):
+        mock_users_data = [
+            {'name': 'test 1', 'email': 'test@user1.com', 'password': 'test'},
+            {'name': 'test 2', 'email': 'test@user2.com', 'password': 'test'},
+            {'name': 'test 3', 'email': 'test@user3.com', 'password': 'test'}
+        ]
+
+        for mock_data in mock_users_data:
+            user = UserSchema.create(**mock_data)
+            self.session.add(user)
+
+        self.session.commit()
+
+    def tearDown(self):
+        self.session.close()
+        kiskadee.model.Base.metadata.drop_all()
+
+    # GET /users -> 200 ok
+    def test_get_users(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.get("/users")
+        data = json.loads(response.data.decode("utf-8"))
+        total_users_count = self.session.query(User).count()
+
+        self.assertIn("users", data)
+        self.assertEqual(len(data['users']), total_users_count)
+        self.assertEqual(200, response.status_code)
+
+        # no password field is given
+        user = data['users'][0]
+        self.assertIsNone(user.get('password'))
+        self.assertIsNone(user.get('password_hash'))
+
+
+    # POST /users -> 201 created
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_successful_create_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        new_user_data = {
+            'name': 'new user',
+            'email': 'new@user.com',
+            'password': 'new user'
+        }
+
+        total_users_before_creation = self.session.query(User).count()
+        response = self.app.post("/users",
+                                 data=json.dumps(new_user_data),
+                                 content_type='application/json')
+        total_users_after_creation = self.session.query(User).count()
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(total_users_after_creation, total_users_before_creation + 1)
+        self.assertEqual(data['user']['email'], new_user_data['email'])
+        self.assertEqual(201, response.status_code)
+
+    # POST /users -> 400 Bad Request
+    def test_missing_arguments_create_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        new_user_data = {
+            'name': 'new user',
+            'password': 'new user'
+        }
+
+        total_users_before_creation = self.session.query(User).count()
+        response = self.app.post("/users",
+                                 data=json.dumps(new_user_data),
+                                 content_type='application/json')
+        total_users_after_creation = self.session.query(User).count()
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertIn("validations", data)
+        self.assertEqual(total_users_after_creation, total_users_before_creation)
+        self.assertEqual(data['error'], 'Validation error')
+        self.assertEqual(data['validations']['email'][0],
+                         'Missing data for required field.')
+        self.assertEqual(400, response.status_code)
+
+    # POST /users -> 403 Forbidden
+    def test_already_exists_create_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        new_user_data = {
+            'name': 'new user',
+            'password': 'new user',
+            'email': 'test@user1.com'
+        }
+
+        total_users_before_creation = self.session.query(User).count()
+        response = self.app.post("/users",
+                                 data=json.dumps(new_user_data),
+                                 content_type='application/json')
+        total_users_after_creation = self.session.query(User).count()
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(total_users_after_creation, total_users_before_creation)
+        self.assertEqual(data['error'], 'user already exists')
+        self.assertEqual(403, response.status_code)
+
+    # GET /users/:id -> 200 ok
+    def test_get_user_data(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+
+        response = self.app.get("/users/%d" % user.id)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['email'], user.email)
+        self.assertEqual(200, response.status_code)
+
+        # no password field is given
+        self.assertIsNone(data['user'].get('password'))
+        self.assertIsNone(data['user'].get('password_hash'))
+
+    # GET /users/:id -> 404 Not Found
+    def test_not_found_get_user_data(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.get("/users/%d" % 123456789)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'], 'user not found')
+        self.assertEqual(404, response.status_code)
+
+    # PUT /users/:id -> 200 ok
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_successful_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        response = self.app.put("/users/{}".format(user.id),
+                                 data=json.dumps({'email': 'another@email.com',
+                                                  'password': 'password'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['id'], user.id)
+        self.assertEqual(data['user']['email'], 'another@email.com')
+        self.assertEqual(200, response.status_code)
+
+        updated_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertEqual(user.id, updated_user.id)
+        self.assertEqual(user.password_hash, updated_user.password_hash)
+        self.assertEqual(updated_user.email, 'another@email.com')
+
+    # PUT /users/:id -> 200 ok
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_ignores_password_hash_on_ajax_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        response = self.app.put("/users/{}".format(user.id),
+                                 data=json.dumps({'password_hash': 'ignome_me'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['id'], user.id)
+        self.assertEqual(200, response.status_code)
+
+        updated_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertNotEqual(updated_user.password_hash, 'ignome_me')
+        self.assertEqual(updated_user.password_hash, user.password_hash)
+
+    # PUT /users/:id -> 400 Bad Request
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_validation_errors_on_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        response = self.app.put("/users/{}".format(user.id),
+                                 data=json.dumps({'password': 'foo',
+                                                  'email': 'not an email'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertIn("validations", data)
+        self.assertEqual(data['error'], 'Validation error')
+        self.assertEqual(data['validations']['email'][0],
+                         'Not a valid email address')
+        self.assertEqual(data['validations']['password'][0],
+                         'Length must be between 4 and 255.')
+        self.assertEqual(400, response.status_code)
+
+        updated_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertNotEqual(updated_user.email, 'not an email')
+
+
+    # PUT /users/:id -> 404 Not Found
+    def test_not_found_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.put("/users/{}".format(123456789),
+                                 data=json.dumps({'password': 'not found ?'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'], 'user not found')
+        self.assertEqual(404, response.status_code)
+
+    # DELETE /users/:id -> 204 No Content
+    def test_successful_delete_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+
+        response = self.app.delete("/users/{}".format(user.id))
+
+        self.assertEqual(response.data, b'')
+        self.assertEqual(204, response.status_code)
+
+        deleted_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertIsNone(deleted_user)
+
+    # DELETE /users/:id -> 404 Not Found
+    def test_not_found_delete_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.delete("/users/{}".format(123456789))
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'], 'user not found')
+        self.assertEqual(404, response.status_code)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/kiskadee/tests/test_model.py b/kiskadee/tests/test_model.py
index 4a56835..8615c88 100644
--- a/kiskadee/tests/test_model.py
+++ b/kiskadee/tests/test_model.py
@@ -174,6 +174,22 @@ class ModelTestCase(unittest.TestCase):
         self.assertEqual(analysis[0].raw, "<>")
         self.assertEqual(analysis[1].raw, "><")
 
+    def test_it_hash_a_user_password(self):
+        u = model.User()
+
+        old_password = 'foobar'
+        u.password_hash = old_password
+        u.hash_password('test')
+
+        self.assertNotEqual(u.password_hash, old_password)
+        self.assertGreater(len(u.password_hash), 100)
+
+    def test_it_verify_a_user_password(self):
+        u = model.User()
+        u.hash_password('test')
+
+        self.assertTrue(u.verify_password('test'))
+        self.assertFalse(u.verify_password('wrong password'))
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/kiskadee/tests/test_serializers.py b/kiskadee/tests/test_serializers.py
new file mode 100644
index 0000000..a4482e8
--- /dev/null
+++ b/kiskadee/tests/test_serializers.py
@@ -0,0 +1,53 @@
+import unittest
+from marshmallow.exceptions import ValidationError
+
+from kiskadee.api.serializers import UserSchema, User
+
+
+class SerializersTestCase(unittest.TestCase):
+
+    def test_UserSchema_validates_user_data(self):
+        wrong_data = {'name': 'foo', 'email': 'foo', 'password': 'foo'}
+        validation = UserSchema().load(wrong_data)
+
+        self.assertTrue(validation.errors)
+        self.assertEqual(validation.errors['name'][0],
+                         'Length must be between 4 and 255.')
+        self.assertEqual(validation.errors['email'][0],
+                         'Not a valid email address')
+        self.assertEqual(validation.errors['email'][1],
+                         'Length must be between 4 and 255.')
+        self.assertEqual(validation.errors['password'][0],
+                         'Length must be between 4 and 255.')
+
+        validation = UserSchema().load({})
+
+        self.assertEqual(validation.errors['name'][0],
+                         'Missing data for required field.')
+
+        self.assertEqual(validation.errors['email'][0],
+                         'Missing data for required field.')
+
+    def test_UserSchema_create_a_user_instance(self):
+        data = {'name': 'Test', 'email': 'test@email.com', 'password': 'test'}
+        user = UserSchema.create(**data)
+
+        self.assertIsInstance(user, User)
+        self.assertNotEqual(user.password_hash, 'test')
+        self.assertGreater(len(user.password_hash), 100)
+
+    def test_UserSchema_raise_ValidationError_if_data_is_invalid(self):
+        with self.assertRaises(ValidationError) as context:
+            data = {}
+            UserSchema.create(**data)
+
+        errors = context.exception.args[0]
+        self.assertIsInstance(context.exception, ValidationError)
+        self.assertEqual(errors['name'][0],
+                         'Missing data for required field.')
+        self.assertEqual(errors['email'][0],
+                         'Missing data for required field.')
+
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 8303835..07d1751 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,3 +12,6 @@ flask_cors
 Flask-Restless
 marshmallow
 alembic
+sphinx-rtd-theme
+passlib
+bcrypt

From bf14d7cc8c7a13322b8966a62f6414d264929b39 Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Nov 21 2017 11:42:46 +0000
Subject: [PATCH 2/7] Add token generation and verification


---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 2a1e9a9..80ae044 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -5,16 +5,74 @@ from flask_cors import CORS
 from marshmallow.exceptions import ValidationError
 
 from kiskadee.database import Database
-from kiskadee.model import Package, Fetcher, Version, Analysis, User
+from kiskadee.model import Package, Fetcher, Version, Analysis, User,\
+        TOKEN_SECRET_KEY
 from kiskadee.api.serializers import PackageSchema, FetcherSchema,\
         AnalysisSchema, UserSchema
 import json
 
+import jwt
+from functools import wraps
+
 kiskadee = Flask(__name__)
 
 CORS(kiskadee)
 
 
+def token_required(fn):
+    """Token verification decorator. When applyed on a route it will
+    look for the x-access-token on the request header.
+
+    If it is valid, the the route is executed.
+    Else, the token is missing or is invalid or has expired, either way
+    the user receive a 403 status code when invalid.
+
+    Possible status code:
+        - 403 Forbidden ->
+            "Token is missing" or "Token expired" or "Invalid token"
+    """
+    @wraps(fn)
+    def decorated(*args, **kwargs):
+        token = None
+
+        if 'x-access-token' in request.headers:
+            token = request.headers['x-access-token']
+
+        if not token:
+            return make_response(jsonify({'error': 'Token is missing'}), 403)
+
+        try:
+            data = jwt.decode(token, TOKEN_SECRET_KEY)
+        except jwt.ExpiredSignatureError:
+            return make_response(jsonify({
+                    'error': 'Token expired'
+                    }), 403)
+        except jwt.InvalidTokenError:
+            return make_response(jsonify({
+                    'error': 'Invalid token'
+                    }), 403)
+
+        params = dict(kwargs, token_data=data)
+        return fn(*args, **params)
+
+    return decorated
+
+@kiskadee.route('/login', methods=['POST'])
+def login():
+    json_data = request.get_json()
+    email, password = [json_data.get('email'), json_data.get('password')]
+
+    if email and password:
+        db_session = kiskadee_db_session()
+        user = db_session.query(User).filter_by(email=email).first()
+
+        if user is not None and user.verify_password(password):
+            token = user.generate_token()
+            return make_response(jsonify({'token': token}), 200)
+
+    return make_response(jsonify({'error': 'Could not verify !'}), 401)
+
+
 @kiskadee.route('/fetchers')
 def index():
     """Get the list of available fetchers."""
@@ -66,7 +124,8 @@ def package_analysis(pkg_name, version):
 
 
 @kiskadee.route('/users', methods=['GET'])
-def get_users():
+@token_required
+def get_users(token_data):
     """Get the list of users
 
     GET /users
diff --git a/kiskadee/model.py b/kiskadee/model.py
index 86f2365..b6cab39 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -6,8 +6,13 @@ from sqlalchemy import Column, Integer, UnicodeText, UniqueConstraint,\
 from passlib.apps import custom_app_context as pwd_context
 import kiskadee
 
-Base = declarative_base()
+import jwt
+import os
+import datetime
+
+TOKEN_SECRET_KEY = os.getenv('TOKEN_SECRET_KEY', 'default development key')
 
+Base = declarative_base()
 
 class Package(Base):
     """Software packages abstraction.
@@ -128,3 +133,12 @@ class User(Base):
         False if not.
         """
         return pwd_context.verify(password, self.password_hash)
+
+    def generate_token(self):
+        """Generates user auth token and returns it"""
+        token = jwt.encode({
+            'user_id': self.id,
+            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
+        }, TOKEN_SECRET_KEY)
+
+        return token.decode('UTF-8')
\ No newline at end of file
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index 70e24c8..b8862a9 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -53,7 +53,11 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
-        response = self.app.get("/users")
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        response = self.app.get("/users",
+                                headers={'x-access-token': user_token})
         data = json.loads(response.data.decode("utf-8"))
         total_users_count = self.session.query(User).count()
 
diff --git a/kiskadee/tests/test_model.py b/kiskadee/tests/test_model.py
index 8615c88..0ea53f0 100644
--- a/kiskadee/tests/test_model.py
+++ b/kiskadee/tests/test_model.py
@@ -1,6 +1,7 @@
 import unittest
 from sqlalchemy import exc
 from sqlalchemy.orm import sessionmaker
+import jwt
 
 from kiskadee import model
 from kiskadee.database import Database
@@ -191,5 +192,20 @@ class ModelTestCase(unittest.TestCase):
         self.assertTrue(u.verify_password('test'))
         self.assertFalse(u.verify_password('wrong password'))
 
+    def test_it_generates_a_user_auth_token(self):
+        u = model.User(name='test', email='test@email.com')
+        u.hash_password('test')
+
+        self.session.add(u)
+        self.session.commit()
+
+        u = self.session.query(model.User)\
+                .filter_by(email='test@email.com').first()
+        token = u.generate_token()
+        decoded_token = jwt.decode(token, model.TOKEN_SECRET_KEY)
+
+        self.assertGreaterEqual(len(token), 121)
+        self.assertEqual(decoded_token['user_id'], u.id)
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/requirements.txt b/requirements.txt
index 07d1751..d8056e4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,3 +15,4 @@ alembic
 sphinx-rtd-theme
 passlib
 bcrypt
+pyjwt
\ No newline at end of file

From 87324c44d1d2de1160d81c0328e3188c145963f6 Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Nov 21 2017 17:23:08 +0000
Subject: [PATCH 3/7] Add token_required to user routes


Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>
Signed-off-by: danmoura17 <danmoura17@gmail.com>

---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 80ae044..bd7f1ae 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -44,13 +44,9 @@ def token_required(fn):
         try:
             data = jwt.decode(token, TOKEN_SECRET_KEY)
         except jwt.ExpiredSignatureError:
-            return make_response(jsonify({
-                    'error': 'Token expired'
-                    }), 403)
+            return make_response(jsonify({'error': 'Token expired'}), 403)
         except jwt.InvalidTokenError:
-            return make_response(jsonify({
-                    'error': 'Invalid token'
-                    }), 403)
+            return make_response(jsonify({'error': 'Invalid token'}), 403)
 
         params = dict(kwargs, token_data=data)
         return fn(*args, **params)
@@ -59,6 +55,14 @@ def token_required(fn):
 
 @kiskadee.route('/login', methods=['POST'])
 def login():
+    """Token based login
+
+    POST /login
+
+    Possible status code:
+        - 200 Ok -> User token
+        - 401 Unauthorized -> Could not log user
+    """
     json_data = request.get_json()
     email, password = [json_data.get('email'), json_data.get('password')]
 
@@ -180,8 +184,9 @@ def create_user():
     return make_response(jsonify({'user': result.data}), 201)
 
 
-@kiskadee.route('/users/<user_id>', methods=['GET'])
-def get_user_data(user_id):
+@kiskadee.route('/users/<int:user_id>', methods=['GET'])
+@token_required
+def get_user_data(token_data, user_id):
     """Get the user data
 
     GET /users/:id
@@ -202,8 +207,9 @@ def get_user_data(user_id):
     return make_response(jsonify({'user': result.data}), 200)
 
 
-@kiskadee.route('/users/<user_id>', methods=['PUT'])
-def update_user(user_id):
+@kiskadee.route('/users/<int:user_id>', methods=['PUT'])
+@token_required
+def update_user(token_data, user_id):
     """Updates a user
 
     PUT /users/:id
@@ -211,6 +217,7 @@ def update_user(user_id):
     Possible status code:
         - 200 Ok -> User updated
         - 400 Bad Request -> Validation error
+        - 403 Forbidden -> Token user does not match to requested user
         - 404 Not Found -> User not found
     """
     db_session = kiskadee_db_session()
@@ -219,6 +226,11 @@ def update_user(user_id):
     if user is None:
         return make_response(jsonify({'error': 'user not found'}), 404)
 
+    if token_data['user_id'] != user_id:
+        return make_response(jsonify({
+            'error': 'token user does not match to requested user'
+            }), 403)
+
     json_data = request.get_json()
     user_data = UserSchema().dump(user).data
     user_data.update(json_data)
@@ -245,14 +257,16 @@ def update_user(user_id):
     return make_response(jsonify({'user': result.data}), 200)
 
 
-@kiskadee.route('/users/<user_id>', methods=['DELETE'])
-def delete_user(user_id):
+@kiskadee.route('/users/<int:user_id>', methods=['DELETE'])
+@token_required
+def delete_user(token_data, user_id):
     """Deletes a user
 
     DELETE /users/:id
 
     Possible status code:
         - 204 No Content -> User deleted
+        - 403 Forbidden -> Token user does not match to requested user
         - 404 Not Found -> User not found
     """
     db_session = kiskadee_db_session()
@@ -261,6 +275,11 @@ def delete_user(user_id):
     if user is None:
         return make_response(jsonify({'error': 'user not found'}), 404)
 
+    if token_data['user_id'] != user_id:
+        return make_response(jsonify({
+            'error': 'token user does not match to requested user'
+            }), 403)
+
     db_session.delete(user)
     db_session.commit()
 
diff --git a/kiskadee/model.py b/kiskadee/model.py
index b6cab39..c7bc9ca 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -132,7 +132,10 @@ class User(Base):
         True if the password is correct
         False if not.
         """
-        return pwd_context.verify(password, self.password_hash)
+        try:
+            return pwd_context.verify(password, self.password_hash)
+        except ValueError:
+            return False
 
     def generate_token(self):
         """Generates user auth token and returns it"""
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index b8862a9..2188c22 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -46,6 +46,66 @@ class ApiUsersTestCase(unittest.TestCase):
         self.session.close()
         kiskadee.model.Base.metadata.drop_all()
 
+    # POST /login -> 200 Ok
+    def test_get_user_token_on_login(self):
+        kiskadee.api.app.kiskadee_db_session = lambda: self.session
+
+        user_data = {
+            'name': 'login',
+            'email': 'login@email.com',
+            'password':'login'
+        }
+
+        # Creating a user as user.verify_password inside login route
+        # gives ValueError with users created with mock_hash_password
+        user = UserSchema.create(**user_data)
+        self.session.add(user)
+        self.session.commit()
+
+        login_data = {
+            'email': user_data['email'],
+            'password': user_data['password']
+        }
+
+        response = self.app.post("/login",
+                                 data=json.dumps(login_data),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("token", data)
+        self.assertEqual(200, response.status_code)
+
+    # POST /login -> 401 Unauthorized
+    def test_wrong_data_on_user_login_gives_unauthorized_response(self):
+        kiskadee.api.app.kiskadee_db_session = lambda: self.session
+
+        user_data = {
+            'name': 'login',
+            'email': 'login@email.com',
+            'password':'login'
+        }
+
+        user = UserSchema.create(**user_data)
+        self.session.add(user)
+        self.session.commit()
+
+        login_data = {
+            'email': user_data['email'],
+            'password': 'not my password'
+        }
+
+        response = self.app.post("/login",
+                                 data=json.dumps(login_data),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data["error"], "Could not verify !")
+        self.assertEqual(401, response.status_code)
+
+
     # GET /users -> 200 ok
     def test_get_users(self):
         def mock_kiskadee_db_session():
@@ -160,8 +220,10 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
 
-        response = self.app.get("/users/%d" % user.id)
+        response = self.app.get("/users/%d" % user.id,
+                                headers={'x-access-token': user_token})
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("user", data)
@@ -179,7 +241,11 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
-        response = self.app.get("/users/%d" % 123456789)
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        response = self.app.get("/users/%d" % 123456789,
+                                headers={'x-access-token': user_token})
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("error", data)
@@ -195,10 +261,13 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(user.id),
                                  data=json.dumps({'email': 'another@email.com',
                                                   'password': 'password'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -222,9 +291,12 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(user.id),
                                  data=json.dumps({'password_hash': 'ignome_me'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -246,10 +318,13 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(user.id),
                                  data=json.dumps({'password': 'foo',
                                                   'email': 'not an email'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -266,6 +341,43 @@ class ApiUsersTestCase(unittest.TestCase):
 
         self.assertNotEqual(updated_user.email, 'not an email')
 
+    # PUT /users/:id -> 403 Forbidden
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_only_the_token_user_can_updates_its_data(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        def send_request(user, data, token):
+            return self.app.put("/users/{}".format(user.id),
+                                data=json.dumps(data),
+                                content_type='application/json',
+                                headers={'x-access-token': token})
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        user_to_update = self.session.query(User).\
+                            order_by(User.id.desc()).\
+                            first()
+
+        response = send_request(user_to_update, {'name': 'test'}, user_token)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'],
+            'token user does not match to requested user')
+        self.assertEqual(403, response.status_code)
+
+        token = user_to_update.generate_token()
+        response = send_request(user_to_update, {'name': 'new name'}, token)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['id'], user_to_update.id)
+        self.assertEqual(data['user']['name'], 'new name')
+        self.assertEqual(200, response.status_code)
 
     # PUT /users/:id -> 404 Not Found
     def test_not_found_update_user(self):
@@ -274,9 +386,13 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(123456789),
                                  data=json.dumps({'password': 'not found ?'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -292,8 +408,10 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
 
-        response = self.app.delete("/users/{}".format(user.id))
+        response = self.app.delete("/users/{}".format(user.id),
+                                    headers={'x-access-token': user_token})
 
         self.assertEqual(response.data, b'')
         self.assertEqual(204, response.status_code)
@@ -302,6 +420,28 @@ class ApiUsersTestCase(unittest.TestCase):
 
         self.assertIsNone(deleted_user)
 
+    # DELETE /users/:id -> 403 Forbidden
+    def test_only_the_token_user_can_delete_it_self(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        other_user = self.session.query(User).order_by(User.id.desc()).first()
+
+        response = self.app.delete("/users/{}".format(other_user.id),
+                                    headers={'x-access-token': user_token})
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'],
+            'token user does not match to requested user')
+        self.assertEqual(403, response.status_code)
+
     # DELETE /users/:id -> 404 Not Found
     def test_not_found_delete_user(self):
         def mock_kiskadee_db_session():
@@ -309,7 +449,11 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
-        response = self.app.delete("/users/{}".format(123456789))
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        response = self.app.delete("/users/{}".format(123456789),
+                                    headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
diff --git a/requirements.txt b/requirements.txt
index d8056e4..b3c71ee 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,6 @@ flask_cors
 Flask-Restless
 marshmallow
 alembic
-sphinx-rtd-theme
 passlib
 bcrypt
 pyjwt
\ No newline at end of file

From 6e8cb1c665d38736eefa80de6bc392ab765445d8 Mon Sep 17 00:00:00 2001
From: danmoura17 <danmoura17@gmail.com>
Date: Nov 21 2017 18:54:00 +0000
Subject: [PATCH 4/7] Set auth token to expire after 48 hours


Signed-off-by: danmoura17 <danmoura17@gmail.com>
Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>

---

diff --git a/kiskadee/model.py b/kiskadee/model.py
index c7bc9ca..4e879ec 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -141,7 +141,7 @@ class User(Base):
         """Generates user auth token and returns it"""
         token = jwt.encode({
             'user_id': self.id,
-            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
+            'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=48)
         }, TOKEN_SECRET_KEY)
 
-        return token.decode('UTF-8')
\ No newline at end of file
+        return token.decode('UTF-8')

From e559e7fb2efcf85cace654a08f03c07c174299cb Mon Sep 17 00:00:00 2001
From: danmoura17 <danmoura17@gmail.com>
Date: Nov 21 2017 19:05:43 +0000
Subject: [PATCH 5/7] Pass user_id on login together with token


Signed-off-by: danmoura17 <danmoura17@gmail.com>
Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>

---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index bd7f1ae..e5ff6c3 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -72,7 +72,8 @@ def login():
 
         if user is not None and user.verify_password(password):
             token = user.generate_token()
-            return make_response(jsonify({'token': token}), 200)
+            response = {'token': token, 'user_id': user.id}
+            return make_response(jsonify(response), 200)
 
     return make_response(jsonify({'error': 'Could not verify !'}), 401)
 
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index 2188c22..9955be5 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -74,6 +74,8 @@ class ApiUsersTestCase(unittest.TestCase):
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("token", data)
+        self.assertIn("user_id", data)
+        self.assertEqual(user.id, data['user_id'])
         self.assertEqual(200, response.status_code)
 
     # POST /login -> 401 Unauthorized

From 1f982a6b0883497180b3d7970a3ae9822e404302 Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Nov 23 2017 19:39:35 +0000
Subject: [PATCH 6/7] Pass user token on creation


---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index e5ff6c3..ca2aeb2 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -182,7 +182,9 @@ def create_user():
     user_schema = UserSchema()
     result = user_schema.dump(user)
 
-    return make_response(jsonify({'user': result.data}), 201)
+    token = user.generate_token()
+
+    return make_response(jsonify({'user': result.data, 'token': token}), 201)
 
 
 @kiskadee.route('/users/<int:user_id>', methods=['GET'])
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index 9955be5..d0f0383 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -156,6 +156,7 @@ class ApiUsersTestCase(unittest.TestCase):
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("user", data)
+        self.assertIn("token", data)
         self.assertEqual(total_users_after_creation, total_users_before_creation + 1)
         self.assertEqual(data['user']['email'], new_user_data['email'])
         self.assertEqual(201, response.status_code)

From a733290598d7d03d4eb200ed8de61044264ae1b4 Mon Sep 17 00:00:00 2001
From: DaniloBarros <dan.b412@gmail.com>
Date: Nov 30 2017 21:08:33 +0000
Subject: [PATCH 7/7] Add fields in response of login post method


- user id, user name and user email

Signed-off-by: DaniloBarros <dan.b412@gmail.com>
Signed-off-by: thiagocmoreira <thiago.cmoreira@yahoo.com.br>

---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index ca2aeb2..01b90cc 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -72,7 +72,7 @@ def login():
 
         if user is not None and user.verify_password(password):
             token = user.generate_token()
-            response = {'token': token, 'user_id': user.id}
+            response = {'token': token, 'user': {'id': user.id, 'name': user.name, 'email': user.email}}
             return make_response(jsonify(response), 200)
 
     return make_response(jsonify({'error': 'Could not verify !'}), 401)

