diff --git a/.coverage b/.coverage index 2eaa58b..5b6d14f 100644 --- a/.coverage +++ b/.coverage @@ -1 +1 @@ -!coverage.py: This is a private format, don't read it directly!{"arcs":{"/home/ipaullly/dev/parcels/sendIT/app/utilities/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/validation_functions.py":[[-1,1],[1,3],[3,10],[10,17],[17,-1],[-3,4],[4,5],[5,6],[6,-3],[5,8],[8,-3],[-10,11],[11,12],[12,13],[13,-10],[-17,18],[18,19],[19,21],[21,22],[22,23],[23,-17],[12,15],[15,-10]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/JWT_token.py":[]}} \ No newline at end of file +!coverage.py: This is a private format, don't read it directly!{"arcs":{"/home/ipaullly/dev/parcels/sendIT/app/__init__.py":[[-1,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,16],[16,-1]],"/home/ipaullly/dev/parcels/sendIT/app/config.py":[[-1,1],[1,3],[-3,3],[3,6],[6,7],[7,8],[8,9],[9,-3],[3,11],[-11,11],[11,14],[14,15],[15,16],[16,-11],[11,18],[-18,18],[18,21],[21,22],[22,23],[23,24],[24,-18],[18,26],[-26,26],[26,29],[29,30],[30,31],[31,-26],[26,34],[34,35],[35,36],[36,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v1/__init__.py":[[-1,1],[1,2],[2,4],[4,6],[6,8],[8,10],[10,11],[11,12],[12,13],[13,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v1/views.py":[[-1,1],[1,2],[2,3],[3,4],[4,6],[6,8],[-8,8],[8,11],[11,12],[12,54],[54,-8],[8,64],[-64,64],[64,67],[67,68],[68,-64],[64,85],[-85,85],[85,88],[88,89],[89,-85],[85,102],[-102,102],[102,105],[105,106],[106,-102],[102,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v1/models.py":[[-2,2],[2,4],[-4,4],[4,7],[7,8],[8,12],[12,28],[28,34],[34,45],[45,56],[56,-4],[4,-2],[-8,9],[9,10],[10,-8]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/validation_functions.py":[[-1,1],[1,3],[3,7],[7,14],[14,22],[22,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v2/__init__.py":[[-1,1],[1,2],[2,4],[4,7],[7,9],[9,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],[17,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v2/views.py":[[-1,1],[1,2],[2,6],[6,7],[7,8],[8,10],[10,12],[12,29],[-29,29],[29,32],[32,33],[33,119],[119,-29],[29,133],[-133,133],[133,136],[136,137],[137,-133],[133,156],[-156,156],[156,159],[159,160],[160,-156],[156,199],[-199,199],[199,202],[202,203],[203,-199],[199,249],[-249,249],[249,252],[252,253],[253,-249],[249,292],[-292,292],[292,295],[295,296],[296,-292],[292,328],[-328,328],[328,331],[331,332],[332,-328],[328,-1]],"/home/ipaullly/dev/parcels/sendIT/app/utilities/token_function.py":[[-1,1],[1,2],[2,4],[4,-1]],"/home/ipaullly/dev/parcels/sendIT/app/api/v2/models.py":[[-2,2],[2,4],[-4,4],[4,7],[7,8],[8,29],[29,36],[36,45],[45,61],[61,76],[76,91],[91,103],[103,-4],[4,-2]],"/home/ipaullly/dev/parcels/sendIT/app/api/v2/dbmodel.py":[[-1,1],[1,2],[2,3],[3,5],[-5,5],[5,8],[8,9],[9,17],[17,42],[42,50],[50,59],[59,67],[67,76],[76,84],[84,93],[93,-5],[5,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v1/__init__.py":[[-1,1],[1,2],[2,4],[4,6],[6,8],[8,10],[10,11],[11,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v1/views.py":[[-1,1],[1,2],[2,3],[3,4],[4,6],[-6,6],[6,9],[9,10],[10,-6],[6,47],[-47,47],[47,50],[50,51],[51,-47],[47,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v1/models.py":[[-1,1],[1,2],[2,3],[3,4],[4,6],[-6,6],[6,9],[9,10],[10,15],[15,-6],[6,18],[-10,11],[11,12],[12,13],[13,14],[14,-10],[18,20],[-20,20],[20,23],[23,24],[24,-20],[20,31],[-31,31],[31,34],[34,35],[35,42],[42,52],[52,60],[60,67],[67,86],[86,-31],[31,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v2/__init__.py":[[-1,1],[1,2],[2,4],[4,6],[6,8],[8,10],[10,11],[11,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v2/views.py":[[-1,1],[1,2],[2,5],[5,6],[6,8],[8,10],[-10,10],[10,13],[13,14],[14,-10],[10,58],[-58,58],[58,61],[61,62],[62,-58],[58,-1]],"/home/ipaullly/dev/parcels/sendIT/app/auth/v2/models.py":[[-1,1],[1,2],[2,3],[3,4],[4,7],[7,11],[-11,11],[11,14],[14,15],[15,27],[27,34],[34,42],[42,51],[51,-11],[11,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/__init__.py":[[-1,1],[1,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/test_create_parcel_edgecases.py":[[-1,1],[1,2],[2,5],[5,6],[6,8],[-8,8],[8,11],[11,12],[12,54],[54,57],[57,62],[62,67],[67,72],[72,77],[77,-8],[8,84],[84,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/test_edgecases.py":[[-1,1],[1,2],[2,5],[5,6],[6,8],[-8,8],[8,11],[11,12],[12,46],[46,49],[49,58],[58,64],[64,70],[70,77],[77,83],[83,-8],[8,89],[89,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/test_login.py":[[-1,1],[1,2],[2,5],[5,6],[6,8],[-8,8],[8,11],[11,12],[12,25],[25,35],[35,42],[42,-8],[8,45],[45,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/test_parcels.py":[[-1,1],[1,2],[2,5],[5,6],[6,8],[-8,8],[8,11],[11,12],[12,27],[27,30],[30,39],[39,50],[50,60],[60,70],[70,-8],[8,82],[82,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/test_register.py":[[-1,1],[1,2],[2,5],[5,6],[6,8],[-8,8],[8,11],[11,12],[12,25],[25,29],[29,35],[35,-8],[8,43],[43,-1]],"/home/ipaullly/dev/parcels/sendIT/app/tests/v1/__init__.py":[],"/home/ipaullly/dev/parcels/sendIT/app/tests/v2/__init__.py":[]}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 33b3291..97d8ce4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ venv .vscode *.pytest_cache .travis.yml +*.pytest_cache +node_modules diff --git a/Procfile b/Procfile index 62e430a..8abb376 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: gunicorn 'app:create_app()' \ No newline at end of file +web: gunicorn 'app:create_app(config_option="ProdConfig")' \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 6e73b48..6decbce 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,5 @@ from flask import Flask, Blueprint +from flask_cors import CORS from app import config from app.api.v1 import version1 from app.api.v2 import version2 @@ -19,12 +20,14 @@ def create_app(config_option="DevConfig"): """ app = Flask(__name__) app.config.from_object(config.config[config_option]) - SenditDb.start_db(app.config['DATABASE_URI']) + #SenditDb.start_db(app.config['DATABASE_URI']) + SenditDb.start_db('postgres://vhgobajxbehppp:b05d8b59b7552e74c763d86945c5b29c2fd11c5e000cfc417190eca27f8eb3d5@ec2-54-204-40-248.compute-1.amazonaws.com:5432/d4v7duckviarp9') SenditDb.build_all() app.register_blueprint(version1) app.register_blueprint(version2) app.register_blueprint(auth) app.register_blueprint(auth2) + CORS(app) return app diff --git a/app/api/v2/__init__.py b/app/api/v2/__init__.py index 358a56f..819564d 100644 --- a/app/api/v2/__init__.py +++ b/app/api/v2/__init__.py @@ -1,7 +1,8 @@ from flask import Blueprint from flask_restful import Api #from ...api.v2.views import ParcelList, ParcelDestination, ParcelStatus, ParcelCurrentLocation, CancelParcel, UserOrders -from app.api.v2.views import ParcelList, ParcelDestination, ParcelStatus, ParcelCurrentLocation, CancelParcel, UserOrders +from app.api.v2.views import ParcelList, ParcelDestination, ParcelStatus, \ +ParcelCurrentLocation, CancelParcel, UserOrders, IndividualParcel version2 = Blueprint('v2', __name__, url_prefix="/api/v2") @@ -12,4 +13,5 @@ api.add_resource(ParcelStatus, '/parcels//status') api.add_resource(ParcelCurrentLocation, '/parcels//presentLocation') api.add_resource(CancelParcel, '/parcels//cancel') -api.add_resource(UserOrders, '/users//parcels') \ No newline at end of file +api.add_resource(UserOrders, '/users//parcels') +api.add_resource(IndividualParcel, '/parcels/') \ No newline at end of file diff --git a/app/api/v2/dbmodel.py b/app/api/v2/dbmodel.py index acda444..a5de5c5 100644 --- a/app/api/v2/dbmodel.py +++ b/app/api/v2/dbmodel.py @@ -33,7 +33,7 @@ def build_all(cls): CREATE TABLE IF NOT EXISTS users ( id serial PRIMARY KEY NOT NULL, email character varying(50) NOT NULL, - role BIT, + role character varying(50), password character varying(300) NOT NULL );""" ) @@ -48,7 +48,7 @@ def persist_to_db(cls, query_string, tuple_data): cls.conn.commit() @classmethod - def add_to_db(cls, query_string, tuple_data): + def insert_fetch_from_db(cls, query_string, tuple_data): """ method that saves queries into the database """ @@ -70,7 +70,8 @@ def retrieve_one(cls, query_string): method returns data on a particular row from the database """ cls.cur.execute(query_string) - return cls.cur.fetchone() + result = cls.cur.fetchone() + return result @classmethod def retrieve_all(cls, query_string): diff --git a/app/api/v2/models.py b/app/api/v2/models.py index 3526fbc..c66831c 100644 --- a/app/api/v2/models.py +++ b/app/api/v2/models.py @@ -34,6 +34,23 @@ def order_list(self): resp = SenditDb.retrieve_all(query) return resp + def order_identification(self): + """ + retrieves object containing user and order ids + """ + query = """SELECT user_id, order_id FROM orders ORDER BY order_id ASC;""" + resp = SenditDb.retrieve_all(query) + return resp + + def retrieve_single_order(self, parcel_id): + """ + retrieves an order by id + """ + + query = """SELECT * FROM orders WHERE order_id={}""".format(parcel_id) + resp = SenditDb.retrieve_one(query) + return resp + def update_destination(self, new_dest, parcel_id): """ updates the destination of a user's parcels @@ -41,6 +58,7 @@ def update_destination(self, new_dest, parcel_id): payload = { "updated_destination" : new_dest } + input_query = """SELECT destination FROM orders WHERE order_id={}""".format(parcel_id) response = SenditDb.retrieve_one(input_query) if not response: diff --git a/app/api/v2/views.py b/app/api/v2/views.py index 65a4a91..c83937c 100644 --- a/app/api/v2/views.py +++ b/app/api/v2/views.py @@ -6,8 +6,10 @@ from app.utilities.token_function import decode_token from app.utilities.validation_functions import check_for_space, check_createparcel_keys from app.api.v2.models import OrderParcel +from app.auth.v2.models import User order = OrderParcel() +user = User() class ParcelList(Resource): """ @@ -17,7 +19,6 @@ def post(self): """ post method to add new order to list of orders """ - """ auth_header = request.headers.get('Authorization') if not auth_header: return make_response(jsonify({ @@ -35,14 +36,14 @@ def post(self): return make_response(jsonify({ "message" : "Invalid token type" }), 400) - """ + try: data = request.get_json() item = data['item'] pickup = data['pickup'] dest = data['dest'] pricing = data['pricing'] - author = data['user_id'] + #author = data['user_id'] status = "pending" current_location = "sendIT HQ" except Exception: @@ -55,11 +56,6 @@ def post(self): "message" : "pricing field can only contain numbers" }), 400) - if not author.isdigit(): - return make_response(jsonify({ - "message" : "user field can only contain a numeral" - }), 400) - if not check_for_space(item): return make_response(jsonify({ "message" : "Invalid item name format" @@ -80,14 +76,7 @@ def post(self): "message" : "Invalid price value" }), 400) - - if not check_for_space(author): - return make_response(jsonify({ - "message" : "Invalid user id" - }), 400) - - - res = order.create_order(item, pickup, dest, pricing, author, status, current_location) + res = order.create_order(item, pickup, dest, pricing, user_id, status, current_location) if res == "User already ordered this item": return make_response(jsonify({ @@ -113,6 +102,28 @@ def get(self): "message" : "No orders in the database" }), 400) +class IndividualParcel(Resource): + """ + class for API endpoints for retrieving single order and cancelling particular order + """ + def get(self, id): + """ + get method to retrieve order by id + """ + + individ_order = order.order_list() + + for parcel in individ_order: + if parcel["order_id"] == id: + return make_response(jsonify({ + "message" : "Ok", + "order" : parcel + }), 200) + else: + response = { + "message" : "Invalid id" + } + return make_response(jsonify(response), 400) class ParcelDestination(Resource): """ @@ -140,14 +151,29 @@ def put(self, id): "message" : "Invalid token type" }), 400) - new_destination = request.get_json()['new_destination'] + individ_order = order.order_identification() + + for parcel in individ_order: + if parcel["order_id"] == id: + if not parcel["user_id"] == user_id: + return make_response(jsonify({ + "message" : "Sorry you cannot edit the destination of orders you did not place." + }), 400) + + try: + new_destination = request.get_json()['new_destination'] + except Exception: + return make_response(jsonify({ + "message" : "invalid new destination key" + }), 400) + if not check_for_space(new_destination): return make_response(jsonify({ "message" : "Invalid destination value" }), 400) - + updated_parcel = order.update_destination(new_destination, id) if updated_parcel: return make_response(jsonify({ @@ -184,7 +210,20 @@ def put(self, id): return make_response(jsonify({ "message" : "Invalid token type" }), 400) - order_status = request.get_json()['status'] + + admin = user.check_admin(user_id) + + if not admin: + return make_response(jsonify({ + "message" : "action only accessible to accounts with admin privileges" + }), 403) + + try: + order_status = request.get_json()['status'] + except Exception: + return make_response(jsonify({ + "message" : "invalid status key" + }), 400) if order_status == 'In transit' or order_status == 'Arrived': @@ -228,8 +267,20 @@ def put(self, id): return make_response(jsonify({ "message" : "Invalid token type" }), 400) + + admin = user.check_admin(user_id) - order_location = request.get_json()['current_location'] + if not admin: + return make_response(jsonify({ + "message" : "action only accessible to accounts with admin privileges" + }), 403) + + try: + order_location = request.get_json()['current_location'] + except Exception: + return make_response(jsonify({ + "message" : "invalid current location key" + }), 400) new_location = order.update_current_location(order_location, id) if new_location: diff --git a/app/auth/v2/models.py b/app/auth/v2/models.py index caaba9d..5c42487 100644 --- a/app/auth/v2/models.py +++ b/app/auth/v2/models.py @@ -12,17 +12,41 @@ class User: """ class to register user and generate tokens """ - def add_user(self, email, password): + def add_user(self, email, password, role): hashed_password = generate_password_hash(password) + + user_query = """INSERT INTO users (email, password, role) VALUES (%s, %s, %s) RETURNING email, role, id""" + tup = (email, hashed_password, role) + resp = SenditDb.insert_fetch_from_db(user_query, tup) + payload = resp + return payload + + def check_duplicate_email(self, email): + email_query = """SELECT * FROM users WHERE email = '{}'""".format(email) duplicate_email = SenditDb.retrieve_all(email_query) if duplicate_email: - return False - user_query = """INSERT INTO users (email, password) VALUES (%s, %s) RETURNING email, id""" - tup = (email, hashed_password) - resp = SenditDb.add_to_db(user_query, tup) - payload = resp - return payload + return True + + def check_admin(self, user_id): + """ + method checks whether a user is an admin by id + """ + admin_query = """SELECT role FROM users WHERE id = {}""".format(user_id) + user_role = SenditDb.retrieve_all(admin_query) + if user_role[0]['role'] == 'admin': + return True + + def check_role(self): + """ + method returns True if an account with admin privileges already exists in the database + """ + role_query = """SELECT role FROM users ORDER BY id ASC;""" + user_roles = SenditDb.retrieve_all(role_query) + print(user_roles) + for role in user_roles: + if role['role'] == 'admin': + return True def get_user_by_email(self, email): email_query = """SELECT * FROM users WHERE email = '{}'""".format(email) @@ -30,6 +54,13 @@ def get_user_by_email(self, email): if not response: return False return response + + def get_email_by_id(self, id): + query = """SELECT email FROM users WHERE id = '{}'""".format(id) + response = SenditDb.retrieve_all(query) + if not response: + return False + return response def validate_password(self, password, user_email): @@ -49,13 +80,9 @@ def generate_token(self, userID): 'iat' : datetime.utcnow(), 'id' : userID } - token = jwt.encode( - payload, - os.environ.get('SECRET_KEY'), - algorithm='HS256' - ) + token = jwt.encode(payload, os.environ.get('SECRET_KEY')) return token - except Exception as err: - return str(err) + except Exception: + return "problem with the token generation" diff --git a/app/auth/v2/views.py b/app/auth/v2/views.py index 671b895..4c7dcb1 100644 --- a/app/auth/v2/views.py +++ b/app/auth/v2/views.py @@ -1,3 +1,4 @@ +import codecs from flask_restful import Resource from flask import make_response,jsonify, request #from ...auth.v2.models import User @@ -40,16 +41,23 @@ def post(self): 'message' : 'Ensure your password is at least 8 charaters and includes an Uppercase letter' } return make_response(jsonify(response), 400) - - new_user = user.add_user(email, password) - - if not new_user: + + checked_email = user.check_duplicate_email(email) + if checked_email: response = { 'message' : 'User with the email already exists' } return make_response(jsonify(response), 400) + + role = user.check_role() + + if role: + user_role = "user" + if not role: + user_role = "admin" - + new_user = user.add_user(email, password, user_role) + return make_response(jsonify({ 'message' : 'you have successfully registered an account', 'data' : new_user @@ -90,6 +98,7 @@ def post(self): 'message' : 'incorrect login credentials. please enter details again' } return make_response(jsonify(response), 401) + auth_token = user.generate_token(user_id) if not auth_token: @@ -97,9 +106,16 @@ def post(self): 'message' : 'token generation failed' } return make_response(jsonify(response), 401) + + #str_token = str(object=auth_token, encoding='utf-8', errors='strict') + #str_token = auth_token.encode() + + token_to_string = str(auth_token) + str_token = ''.join(token_to_string.split('b', 1)) + response = { 'message' : 'Successfully logged in', - 'data' : auth_token.decode() + 'data' : str_token } return make_response(jsonify(response), 200) \ No newline at end of file diff --git a/app/tests/v2/test_create_parcel_edgecases.py b/app/tests/test_create_parcel_edgecases.py similarity index 100% rename from app/tests/v2/test_create_parcel_edgecases.py rename to app/tests/test_create_parcel_edgecases.py diff --git a/app/tests/v2/test_edgecases.py b/app/tests/test_edgecases.py similarity index 100% rename from app/tests/v2/test_edgecases.py rename to app/tests/test_edgecases.py diff --git a/app/tests/v2/test_login.py b/app/tests/test_login.py similarity index 100% rename from app/tests/v2/test_login.py rename to app/tests/test_login.py diff --git a/app/tests/v2/test_parcels.py b/app/tests/test_parcels.py similarity index 100% rename from app/tests/v2/test_parcels.py rename to app/tests/test_parcels.py diff --git a/app/tests/v2/test_register.py b/app/tests/test_register.py similarity index 100% rename from app/tests/v2/test_register.py rename to app/tests/test_register.py diff --git a/app/tests/v1/__init__.py b/app/tests/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/tests/v2/__init__.py b/app/tests/v2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/utilities/token_function.py b/app/utilities/token_function.py index b71265f..03f2584 100644 --- a/app/utilities/token_function.py +++ b/app/utilities/token_function.py @@ -15,3 +15,9 @@ def decode_token(token): except jwt.InvalidTokenError: #the token is not valid, throw error return "Unworthy token. Please login to get fresh authorization" + +def convert_token(auth_token): + """ + function to change generated token from byte to string + """ + return "".join( chr(x) for x in auth_token) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2907d91..8655c4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ coverage==4.5.1 coveralls==1.5.1 docopt==0.6.2 Flask==1.0.2 +Flask-Cors==3.0.7 Flask-RESTful==0.3.6 gunicorn==19.9.0 idna==2.7 @@ -19,7 +20,6 @@ lazy-object-proxy==1.3.1 MarkupSafe==1.0 mccabe==0.6.1 more-itertools==4.3.0 -pkg-resources==0.0.0 pluggy==0.8.0 psycopg2-binary==2.7.6 py==1.7.0