diff --git a/docs/postman.json b/docs/postman.json index f74c7a2..418f46c 100644 --- a/docs/postman.json +++ b/docs/postman.json @@ -6,120 +6,296 @@ }, "item": [ { - "name": "/api/register", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", - "options": { - "raw": { - "language": "json" + "name": "User", + "item": [ + { + "name": "/api/register", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/register", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "register" + ] } - } + }, + "response": [] }, - "url": { - "raw": "{{BASE_URL}}/api/register", - "host": [ - "{{BASE_URL}}" - ], - "path": [ - "api", - "register" - ] + { + "name": "/api/login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/login", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "login" + ] + } + }, + "response": [] + }, + { + "name": "/api/logout", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/logout", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "logout" + ] + } + }, + "response": [] + }, + { + "name": "/api/users", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/users", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "users" + ] + } + }, + "response": [] } - }, - "response": [] + ] }, { - "name": "/api/login", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", - "options": { - "raw": { - "language": "json" + "name": "Share", + "item": [ + { + "name": "/api/shares", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{BASE_URL}}/api/shares", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "shares" + ] } - } + }, + "response": [] }, - "url": { - "raw": "{{BASE_URL}}/api/login", - "host": [ - "{{BASE_URL}}" - ], - "path": [ - "api", - "login" - ] + { + "name": "/api/share", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"symbol\": \"DTEGY\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/share", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "response": [] + }, + { + "name": "/api/share", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"symbol\": \"DTEGY\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/share", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "response": [] } - }, - "response": [] + ] }, { - "name": "/api/logout", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", - "options": { - "raw": { - "language": "json" + "name": "Keyword", + "item": [ + { + "name": "/api/keywords", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE_URL}}/api/keywords", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "keywords" + ] } - } + }, + "response": [] }, - "url": { - "raw": "{{BASE_URL}}/api/logout", - "host": [ - "{{BASE_URL}}" - ], - "path": [ - "api", - "logout" - ] - } - }, - "response": [] - }, - { - "name": "/api/users", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", - "options": { - "raw": { - "language": "json" + { + "name": "/api/keyword", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"Elon\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/keyword", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "keyword" + ] } - } + }, + "response": [] }, - "url": { - "raw": "{{BASE_URL}}/api/users", - "host": [ - "{{BASE_URL}}" - ], - "path": [ - "api", - "users" - ] + { + "name": "/api/keyword", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"Elon\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}/api/keyword", + "host": [ + "{{BASE_URL}}" + ], + "path": [ + "api", + "keyword" + ] + } + }, + "response": [] } - }, - "response": [] + ] } ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IlVzZXJuYW1lIiwiZXhwIjoxNjQ3Mjc1OTI2fQ.MhXTrfeLQmZ8dPzMOcNSTg1PUw4AU-aZ7h_zRmk8ibc", + "type": "string" + } + ] + }, "event": [ { "listen": "prerequest", diff --git a/webservice/app.py b/webservice/app.py index 986853a..1c5c0cc 100644 --- a/webservice/app.py +++ b/webservice/app.py @@ -1,9 +1,11 @@ from flask import Flask from dotenv import load_dotenv -from models import * -from api import api -from interface import interface +from webservice.models import * +from webservice.interface import interface +from webservice.blueprints.keyword import keyword_blueprint +from webservice.blueprints.shares import shares_blueprint +from webservice.blueprints.user import users_blueprint def create_app(): @@ -20,7 +22,12 @@ def create_app(): # Create all tables db.create_all() - application.register_blueprint(api) + # api blueprints + application.register_blueprint(keyword_blueprint) + application.register_blueprint(shares_blueprint) + application.register_blueprint(users_blueprint) + + # interface blueprint application.register_blueprint(interface) return application diff --git a/webservice/blueprints/keyword.py b/webservice/blueprints/keyword.py new file mode 100644 index 0000000..5a01828 --- /dev/null +++ b/webservice/blueprints/keyword.py @@ -0,0 +1,62 @@ +import os + +from flask import Blueprint, jsonify, request + +from webservice.db import db +from webservice.helper_functions import get_username_from_token_data, extract_token_data, get_token, get_user_id_from_username +from webservice.models import Keyword + +keyword_blueprint = Blueprint('keyword', __name__, url_prefix='/api') +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +@keyword_blueprint.route('/keyword', methods=['POST']) +def add_keyword(): + request_data = request.get_json() + key = request_data['keyword'] + + # get username from jwt token + username = get_username_from_token_data(extract_token_data(get_token())) + + check_keyword = db.session.query(Keyword).filter_by(keyword=key, user_id=get_user_id_from_username(username)).first() + if check_keyword is None: + # Keyword doesn't exist yet for this user + new_keyword = Keyword( + user_id=get_user_id_from_username(username), + keyword=key + ) + db.session.add(new_keyword) + db.session.commit() + + return jsonify({"status": 200, "text": "Successfully added keyword", "data": new_keyword.as_dict()}) + else: + return jsonify({"status": 500, "text": "Keyword already exist for this user"}) + + +@keyword_blueprint.route('/keyword', methods=['DELETE']) +def remove_keyword(): + request_data = request.get_json() + key = request_data['keyword'] + + # get username from jwt token + username = get_username_from_token_data(extract_token_data(get_token())) + + db.session.query(Keyword).filter_by(keyword=key, user_id=get_user_id_from_username(username)).delete() + db.session.commit() + + return jsonify({"status": 200, "text": "Successfully removed keyword"}) + + +@keyword_blueprint.route('/keywords', methods=['GET']) +def get_keywords(): + # get username from jwt token + username = get_username_from_token_data(extract_token_data(get_token())) + + return_keywords = [] + keywords = db.session.query(Keyword).filter_by(user_id=get_user_id_from_username(username)).all() + + if keywords is not None: + for row in keywords: + return_keywords.append(row.as_dict()) + + return jsonify({"status": 200, "text": "Successfully loaded keywords", "data": return_keywords}) diff --git a/webservice/blueprints/shares.py b/webservice/blueprints/shares.py new file mode 100644 index 0000000..eb9808c --- /dev/null +++ b/webservice/blueprints/shares.py @@ -0,0 +1,62 @@ +import os + +from flask import Blueprint, jsonify, request + +from webservice.db import db +from webservice.helper_functions import get_username_from_token_data, extract_token_data, get_token, get_user_id_from_username +from webservice.models import Keyword, Share + +shares_blueprint = Blueprint('share', __name__, url_prefix='/api') +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +@shares_blueprint.route('/share', methods=['POST']) +def add_symbol(): + request_data = request.get_json() + symbol = request_data['symbol'] + + # get username from jwt token + username = get_username_from_token_data(extract_token_data(get_token())) + + check_share = db.session.query(Share).filter_by(symbol=symbol, user_id=get_user_id_from_username(username)).first() + if check_share is None: + # Keyword doesn't exist yet for this user + new_symbol = Share( + user_id=get_user_id_from_username(username), + symbol=symbol + ) + db.session.add(new_symbol) + db.session.commit() + + return jsonify({"status": 200, "text": "Successfully added symbol", "data": new_symbol.as_dict()}) + else: + return jsonify({"status": 500, "text": "Symbol already exist for this user"}) + + +@shares_blueprint.route('/share', methods=['DELETE']) +def remove_symbol(): + request_data = request.get_json() + symbol = request_data['symbol'] + + # get username from jwt token + username = get_username_from_token_data(extract_token_data(get_token())) + + db.session.query(Share).filter_by(symbol=symbol, user_id=get_user_id_from_username(username)).delete() + db.session.commit() + + return jsonify({"status": 200, "text": "Successfully removed symbol"}) + + +@shares_blueprint.route('/shares', methods=['GET']) +def get_symbol(): + # get username from jwt token + username = get_username_from_token_data(extract_token_data(get_token())) + + return_symbols = [] + symbols = db.session.query(Share).filter_by(user_id=get_user_id_from_username(username)).all() + + if symbols is not None: + for row in symbols: + return_symbols.append(row.as_dict()) + + return jsonify({"status": 200, "text": "Successfully loaded symbols", "data": return_symbols}) diff --git a/webservice/api.py b/webservice/blueprints/user.py similarity index 68% rename from webservice/api.py rename to webservice/blueprints/user.py index 554e8fb..2926294 100644 --- a/webservice/api.py +++ b/webservice/blueprints/user.py @@ -1,16 +1,18 @@ +import datetime import os +import jwt from flask import Blueprint, jsonify, request -from db import db -from helper_functions import check_password, hash_password -from models import User +from webservice.db import db +from webservice.helper_functions import check_password, hash_password, get_token, extract_token_data +from webservice.models import User -api = Blueprint('api', __name__, url_prefix='/api') +users_blueprint = Blueprint('users', __name__, url_prefix='/api') __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) -@api.route('/users', methods=['GET']) +@users_blueprint.route('/users', methods=['GET']) def users(): res = [] for i in User.query.all(): @@ -19,7 +21,7 @@ def users(): return jsonify({"status": 200, "data": res}) -@api.route('/login', methods=['POST']) +@users_blueprint.route('/login', methods=['POST']) def login(): request_data = request.get_json() username = request_data['username'] @@ -27,19 +29,20 @@ def login(): user = db.session.query(User).filter_by(username=username).first() if check_password(user.password, password): - # TODO Return token - return jsonify({"status": 200, "text": "Successfully logged in"}) + token = jwt.encode({'username': user.username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=45)}, os.getenv('SECRET_KEY'), "HS256") + + return jsonify({"status": 200, "text": "Successfully logged in", "data": token}) else: return jsonify({"status": 500, "text": "Unable to login"}) -@api.route('/logout', methods=['GET']) +@users_blueprint.route('/logout', methods=['GET']) def logout(): # TODO return jsonify({"status": 200, "text": "Successfully logged out"}) -@api.route('/register', methods=['POST']) +@users_blueprint.route('/register', methods=['POST']) def register(): request_data = request.get_json() username = request_data['username'] diff --git a/webservice/helper_functions.py b/webservice/helper_functions.py index 8c9c1b3..1468407 100644 --- a/webservice/helper_functions.py +++ b/webservice/helper_functions.py @@ -1,6 +1,13 @@ import hashlib +import os import uuid +import jwt +from flask import request + +from webservice.db import db +from webservice.models import User + def hash_password(password): salt = uuid.uuid4().hex @@ -9,4 +16,28 @@ def hash_password(password): def check_password(hashed_password, user_password): password, salt = hashed_password.split(':') - return password == hashlib.sha256(salt.encode() + user_password.encode()).hexdigest() \ No newline at end of file + return password == hashlib.sha256(salt.encode() + user_password.encode()).hexdigest() + + +def get_token(): + token = None + if 'Authorization' in request.headers: + token = request.headers['Authorization'].split(" ")[1] + + return token + + +def extract_token_data(token): + if token is not None: + try: + return jwt.decode(token, os.getenv('SECRET_KEY'), algorithms=["HS256"]) + except: + return None + + +def get_username_from_token_data(token_data): + return token_data['username'] + + +def get_user_id_from_username(username): + return db.session.query(User).filter_by(username=username).first().user_id diff --git a/webservice/interface.py b/webservice/interface.py index 0ef449b..c6d95eb 100644 --- a/webservice/interface.py +++ b/webservice/interface.py @@ -8,5 +8,5 @@ __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file # Return all tags @interface.route('/', methods=['GET']) -def get_tags(): - render_template("index.html") +def get_html(): + return render_template("index.html") diff --git a/webservice/requirements.txt b/webservice/requirements.txt index d228ae4..5aa48e2 100644 --- a/webservice/requirements.txt +++ b/webservice/requirements.txt @@ -4,4 +4,5 @@ requests==2.27.1 uwsgi==2.0.20 Flask_SQLAlchemy==2.5.1 python-dotenv==0.19.2 -pymysql==1.0.2 \ No newline at end of file +pymysql==1.0.2 +pyjwt==2.0.0 \ No newline at end of file