From a68f807d65e14224cf346967f52f7d4d4c44d8f0 Mon Sep 17 00:00:00 2001 From: Ken Maina <31338414+KMaina@users.noreply.github.com> Date: Thu, 4 Oct 2018 00:00:33 +0300 Subject: [PATCH] Revert "Revert "#160806671 User Authentication Endpoint"" --- .travis.yml | 6 ++ app/__init__.py | 21 ++++-- app/api/v1/__init__.py | 1 - app/api/v1/views.py | 67 ---------------- app/{tests/v1 => api/v2}/__init__.py | 0 app/api/v2/model.py | 109 +++++++++++++++++++++++++++ app/api/v2/views_register.py | 29 +++++++ app/migration.py | 90 ++++++++++++++++++++++ app/tests/v1/test_views.py | 52 ------------- app/tests/v2/__init__.py | 0 app/tests/v2/test_views_login.py | 46 +++++++++++ app/tests/v2/test_views_register.py | 28 +++++++ requirements.txt | 3 + run.py | 5 -- 14 files changed, 325 insertions(+), 132 deletions(-) delete mode 100644 app/api/v1/__init__.py delete mode 100644 app/api/v1/views.py rename app/{tests/v1 => api/v2}/__init__.py (100%) create mode 100644 app/api/v2/model.py create mode 100644 app/api/v2/views_register.py create mode 100644 app/migration.py delete mode 100644 app/tests/v1/test_views.py create mode 100644 app/tests/v2/__init__.py create mode 100644 app/tests/v2/test_views_login.py create mode 100644 app/tests/v2/test_views_register.py diff --git a/.travis.yml b/.travis.yml index 718d8b1..afdd92f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,16 @@ language: python python: - "3.6" +services: + - postgresql + install: - pip install -r requirements.txt - pip install coveralls +before_script: + - "psql -c 'create database fast_food_app_test;' -U postgres" + script: - coverage run --source=app -m pytest && coverage report diff --git a/app/__init__.py b/app/__init__.py index 148093e..4ee661d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,22 +1,29 @@ """ -the __init__.py file - -included to make app a package +Initializes the flask app and mackes it a package """ +import os from flask import Flask from flask_restful import Api +from flask_jwt_extended import JWTManager -from app.api.v1.views import Orders, OrderSpecific +from app.api.v2.views_register import UserRegister, UserLogin from instance.config import app_config def create_app(config_name): + """Application Factory for the app""" app = Flask(__name__, instance_relative_config=True) app.config.from_object(app_config[config_name]) app.config.from_pyfile('config.py') + app.secret_key = os.getenv('SECRET_KEY') + + #Initialize and use Flask-RESTful api_endpoint = Api(app) - api_endpoint.add_resource(Orders, '/api/v1/orders') - api_endpoint.add_resource(OrderSpecific, '/api/v1/order/') + api_endpoint.add_resource(UserRegister, '/api/v2/auth/signup') + api_endpoint.add_resource(UserLogin, '/api/v2/auth/login') + + #Initialize and use Flask-JWT-Extended + jwt = JWTManager(app) - return app \ No newline at end of file + return app diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py deleted file mode 100644 index 812915b..0000000 --- a/app/api/v1/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Make v1 a module""" diff --git a/app/api/v1/views.py b/app/api/v1/views.py deleted file mode 100644 index 1e0510f..0000000 --- a/app/api/v1/views.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -The views.py file. - -All routes in the app are located here -""" - -from flask import request, json -from flask_restful import Resource - -orders = [] - -class Orders(Resource): - """ - GET/ all orders placed - POST/ a new order - """ - def get(self): - """Return a list of all orders posted""" - if len(orders) == 0: - return {'messsage': 'Nothing found'}, 404 - return orders, 200 - - def post(self): - """Posts a specific order""" - order_data = { - 'name': request.json['name'], - 'quantity' : request.json['quantity'], - 'description' : request.json['description'], - 'id' : len(orders) + 1, - 'status' : request.json['status'] - } - orders.append(order_data) - return order_data, 201 - -class OrderSpecific(Resource): - """ - GET/ a specific order - PUT/ edit a specific order - DELETE/ delete a specific order - """ - def get(self, order_id): - order = [order for order in orders if order['id'] == order_id] - if order: - return {'order': order[0]}, 200 - if not order: - return {'message':'Error, order not found'}, 404 - - def put(self, order_id): - order = [order for order in orders if order['id'] == order_id] - if order: - data = request.get_json() - order[0]['name'] = data['name'] - order[0]['quantity'] = data['quantity'] - order[0]['description'] = data['description'] - order[0]['status'] = data['status'] - return {'order': order[0]}, 200 - if not order: - return {'message':'Error, order not found'}, 404 - - def delete(self, order_id): - order = [order for order in orders if order['id'] == order_id] - if order: - del orders[0] - return {'orders': orders}, 204 - else: - return {'message': 'Could not find your order'}, 404 - diff --git a/app/tests/v1/__init__.py b/app/api/v2/__init__.py similarity index 100% rename from app/tests/v1/__init__.py rename to app/api/v2/__init__.py diff --git a/app/api/v2/model.py b/app/api/v2/model.py new file mode 100644 index 0000000..b07f2dd --- /dev/null +++ b/app/api/v2/model.py @@ -0,0 +1,109 @@ +""" +This module is used to interact with the database +""" +import psycopg2 +from flask_jwt_extended import create_access_token +from flask import request, jsonify + +from app import migration + +connection = migration.db_connection() +cursor = connection.cursor() + +class Users(): + """Class to handle users""" + def register_user(self, username, password, confirm_password, email, address, telephone, admin): + """Method to register a user""" + username = request.json.get('username', None) + password = request.json.get('password', None) + confirm_password = request.json.get('confirm_password', None) + email = request.json.get('email', None) + address = request.json.get('address', None) + telephone = request.json.get('telephone', None) + admin = request.json.get('admin', None) + if not isinstance(username, str): + print(type(username)) + response = jsonify({'msg':'Username must be a string'}) + response.status_code = 400 + return response + if not isinstance(password, str) and isinstance(confirm_password, str): + response = jsonify({'msg':'Password must be a string'}) + response.status_code = 400 + return response + if not isinstance(email, str): + response = jsonify({'msg':'Email must be a string'}) + response.status_code = 400 + return response + if not isinstance(address, str): + response = jsonify({'msg':'Address must be a string'}) + response.status_code = 400 + return response + if not isinstance(telephone, str): + response = jsonify({'msg':'Telephone number must be a string'}) + response.status_code = 400 + return response + if not isinstance(admin, bool): + response = jsonify({'msg':'Admin must be a boolean'}) + response.status_code = 400 + return response + if password != confirm_password: + response = jsonify({'msg':'Passwords must match'}) + response.status_code = 400 + return response + try: + if admin is True: + add_user = "INSERT INTO \ + users \ + (username, password, email, address, telephone, admin) \ + VALUES \ + ('" + username +"', '" + password +"', '" + email + "', \ + '" + address + "', '" + telephone + "', true )" + if admin is False: + add_user = "INSERT INTO \ + users \ + (username, password, email, address, telephone, admin) \ + VALUES ('" + username +"', '" + password +"', '" + email + "', \ + '" + address + "', '" + telephone + "', false )" + cursor.execute(add_user) + connection.commit() + response = jsonify({'msg':'User successfully added to the databse'}) + response.status_code = 201 + return response + except (Exception, psycopg2.DatabaseError) as error: + response = jsonify({'msg':'Problem inserting into the databse'}) + response.status_code = 400 + return response + + def login(self, username, password): + """Method to login a user""" + username = request.json.get('username', None) + password = request.json.get('password', None) + if not isinstance(username, str): + print(type(username)) + response = jsonify({'msg' : 'Username must be a string'}) + response.status_code = 400 + return response + if not isinstance(username, str): + response = jsonify({'msg' : 'Password must be a string'}) + response.status_code = 400 + return response + try: + get_user = "SELECT username, password \ + FROM users \ + WHERE username = '" + username + "' AND password = '" + password + "'" + cursor.execute(get_user) + row = cursor.fetchone() + if row is not None: + row = cursor.fetchone() + access_token = create_access_token(identity=username) + print(access_token) + response = jsonify({"msg":"Successfully logged in", "access_token":access_token}) + response.status_code = 200 + return response + response = jsonify({"msg" : "Error logging in, credentials not found"}) + response.status_code = 401 + return response + except (Exception, psycopg2.DatabaseError) as error: + print("Error executing", error) + return jsonify({"msg" : "Error, check the database {}".format(error)}) + \ No newline at end of file diff --git a/app/api/v2/views_register.py b/app/api/v2/views_register.py new file mode 100644 index 0000000..afafaa1 --- /dev/null +++ b/app/api/v2/views_register.py @@ -0,0 +1,29 @@ +"""The register and user login endpoints""" +from flask import request +from flask_restful import Resource + +from app.api.v2.model import Users + +class UserRegister(Resource): + """This class is used to register a new user""" + def post(self): + """Method to register a user in the app""" + return Users().register_user( + request.json['username'], + request.json['password'], + request.json['confirm_password'], + request.json['email'], + request.json['address'], + request.json['telephone'], + request.json['admin'] + ) + +class UserLogin(Resource): + """This class is used to login a user""" + def post(self): + """Method to log a user into the system.""" + return Users().login( + request.json['username'], + request.json['password'] + ) + \ No newline at end of file diff --git a/app/migration.py b/app/migration.py new file mode 100644 index 0000000..2a38d1e --- /dev/null +++ b/app/migration.py @@ -0,0 +1,90 @@ +""" +File to manage the connection to the database, creation and deletion of tables +""" +import os +import psycopg2 + +def db_connection(config=None): + """Make a connection to the DB""" + if config == 'testing': + db_name = os.getenv('DB_TEST') + else: + db_name = os.getenv('DB_MAIN') + user = os.getenv('DB_USER') + password = os.getenv('DB_PASSWORD') + host = os.getenv('DB_HOST') + port = os.getenv('DB_PORT') + + return psycopg2.connect(user=user, password=password, host=host, port=port, database=db_name) + +def create_tables(cursor): + """Create all tables""" + statements = ( + """ + CREATE TABLE IF NOT EXISTS users ( + user_id SERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + password VARcHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + address VARCHAR(255) NOT NULL, + telephone VARCHAR(255) NOT NULL, + admin BOOLEAN NOT NULL + ) + """, + """ CREATE TABLE IF NOT EXISTS status ( + status_id SERIAL PRIMARY KEY, + status_name VARCHAR(255) NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS menus ( + menu_id SERIAL PRIMARY KEY, + meal_name VARCHAR(255) NOT NULL UNIQUE, + quantity INTEGER NOT NULL, + description VARCHAR(255) NOT NULL, + cost INTEGER NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id SERIAL, + total_cost INT NOT NULL, + status_table_id INT REFERENCES status(status_id) ON DELETE CASCADE, + PRIMARY KEY (order_id, status_table_id) + ) + """) + + for statement in statements: + cursor.execute(statement) + +def drop_tables(cursor): + """Drops all tables""" + drops = ["DROP TABLE users CASCADE", + "DROP TABLE orders CASCADE", + "DROP TABLE menus CASCADE", + "DROP TABLE status CASCADE"] + for drop in drops: + cursor.execute(drop) + cursor.close() + connection.commit() + +def main(config=None): + """ + This function is run in the command line to automate the connection to the database + and creation of tables in the database + """ + connection = db_connection(config=config) + cursor = connection.cursor() + + create_tables(cursor) + + connection.commit() + + cursor.close() + + connection.close() + + print('Done') + +if __name__ == '__main__': + main() diff --git a/app/tests/v1/test_views.py b/app/tests/v1/test_views.py deleted file mode 100644 index 3f7ef87..0000000 --- a/app/tests/v1/test_views.py +++ /dev/null @@ -1,52 +0,0 @@ -"""The test_views.py file - -Runs unit tests on the views.py routes - -""" - -import unittest -import json -from app import create_app - -class TestOrders(unittest.TestCase): - def setUp(self): - self.app = create_app('testing') - self.app.config['TESTING'] = True - self.client = self.app.test_client - self.order = { - "name": "Pizza", - "quantity": 2, - "description": "Perfect as a snack.", - "status": 0 - } - self.changed_order = { - "name": "Pizza", - "quantity": 2, - "description": "Perfect as a snack.", - "status": 1 - } - - def test_add_an_order(self): - """Test for adding an order""" - response = self.client().post('/api/v1/orders', data=json.dumps(self.order), content_type='application/json') - self.assertEqual(response.status_code, 201) - - - def test_get_all_orders(self): - """Test for fetching all orders""" - response = self.client().get('/api/v1/orders') - self.assertEqual(response.status_code, 200) - - def test_get_specific_order(self): - """Test for fetching a specific order""" - response = self.client().get('/api/v1/order/1') - self.assertEqual(response.status_code, 200) - - def test_change_an_order_change(self): - """Test for changing an order""" - response = self.client().post('/api/v1/orders', data=json.dumps(self.order), content_type='application/json') - self.assertEqual(response.status_code, 201) - response = self.client().put('/api/v1/order/1', data=json.dumps(self.changed_order), content_type='application/json') - result = self.client().get('/api/v1/order/1') - self.assertEqual(response.status_code, 200) - self.assertIn('1', str(result.data)) \ No newline at end of file diff --git a/app/tests/v2/__init__.py b/app/tests/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/v2/test_views_login.py b/app/tests/v2/test_views_login.py new file mode 100644 index 0000000..7b97081 --- /dev/null +++ b/app/tests/v2/test_views_login.py @@ -0,0 +1,46 @@ +"""Test for for testing the login endpoint""" +import unittest +import json + +from app import create_app, migration + +class UserLoginTestCase(unittest.TestCase): + """Unit testiing for the user regsitration endpoint""" + def setUp(self): + """Initialize the app and database connections""" + self.app = create_app(config_name="testing") + self.client = self.app.test_client + + with self.app.app_context(): + migration.main(config='testing') + + def test_user_login(self): + """Tests if the correct credentials were supplied""" + response = self.client().post('/api/v2/auth/login', data=json.dumps({ + "username":"molly", + "password":"123", + }), content_type='application/json') + + self.assertEqual(response.status_code, 200) + self.assertIn("Successfully logged in", str(response.data)) + + def test_user_empty_credentials(self): + """Tests if no credentials were supplied""" + response = self.client().post('/api/v2/auth/login', data=json.dumps({ + "username":"", + "password":"", + }), content_type='application/json') + + self.assertEqual(response.status_code, 401) + self.assertIn("Error logging in, credentials not found", str(response.data)) + + def test_user_no_credentials(self): + """Tests if a user does not exist""" + response = self.client().post('/api/v2/auth/login', data=json.dumps({ + "username":"kim", + "password":"pass", + }), content_type='application/json') + + self.assertEqual(response.status_code, 401) + self.assertIn("Error logging in, credentials not found", str(response.data)) + \ No newline at end of file diff --git a/app/tests/v2/test_views_register.py b/app/tests/v2/test_views_register.py new file mode 100644 index 0000000..9dcff8a --- /dev/null +++ b/app/tests/v2/test_views_register.py @@ -0,0 +1,28 @@ +"""Test for for testing the register endpoint""" +import unittest +import json + +from app import create_app, migration + +class UserRegisterTestCase(unittest.TestCase): + """Unit testiing for the user regsitration endpoint""" + def setUp(self): + """Initialize the app and database connections""" + self.app = create_app(config_name="testing") + self.client = self.app.test_client + with self.app.app_context(): + migration.main(config='testing') + + def test_register_user(self): + """Method to test if a user is successfully registered""" + response = self.client().post('/api/v2/auth/signup', data=json.dumps({ + "username" : "molly", + "password" : "123", + "confirm_password" : "123", + "email" : "molly@me.com", + "address" : "Langata, Nairobi", + "telephone" : "+712249175", + "admin" : False + }), content_type='application/json') + + self.assertEqual(response.status_code, 201) diff --git a/requirements.txt b/requirements.txt index 6cc2731..2492f3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ coveralls==1.5.0 decorator==4.3.0 docopt==0.6.2 Flask==1.0.2 +Flask-JWT-Extended==3.13.1 Flask-RESTful==0.3.6 gunicorn==19.9.0 idna==2.7 @@ -28,8 +29,10 @@ parso==0.3.1 pickleshare==0.7.4 pluggy==0.7.1 prompt-toolkit==1.0.15 +psycopg2==2.7.5 py==1.6.0 Pygments==2.2.0 +PyJWT==1.6.4 pylint==2.1.1 pytest==3.8.0 python-dotenv==0.9.1 diff --git a/run.py b/run.py index a3adde8..920fcd9 100644 --- a/run.py +++ b/run.py @@ -1,8 +1,3 @@ -# from app import app - -# if __name__ == 'main': -# app.run() - import os from app import create_app