Extracted frontend from webservice to new directory
Updated directory structure Updated .gitignore
This commit is contained in:
19
api/Dockerfile
Normal file
19
api/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.10-alpine
|
||||
|
||||
WORKDIR /srv/flask_app
|
||||
RUN apk add nginx build-base libffi-dev curl uwsgi
|
||||
|
||||
COPY api/requirements.txt /srv/flask_app/
|
||||
|
||||
RUN pip install -r requirements.txt --src /usr/local/src --no-warn-script-location
|
||||
|
||||
COPY api /srv/flask_app
|
||||
COPY api/deploy/nginx.conf /etc/nginx
|
||||
RUN chmod +x ./deploy/start.sh
|
||||
RUN chmod +x ./deploy/healthcheck.sh
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=2s CMD ["./deploy/healthcheck.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["./deploy/start.sh"]
|
83
api/api_blueprint_keyword.py
Normal file
83
api/api_blueprint_keyword.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint, abort
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from auth import auth
|
||||
from scheme import KeywordResponseSchema, KeywordSchema, DeleteSuccessfulSchema
|
||||
from models import Keyword
|
||||
|
||||
keyword_blueprint = APIBlueprint('keyword', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@keyword_blueprint.route('/keyword', methods=['POST'])
|
||||
@keyword_blueprint.output(KeywordResponseSchema(many=True), 200)
|
||||
@keyword_blueprint.input(schema=KeywordSchema)
|
||||
@keyword_blueprint.auth_required(auth)
|
||||
@keyword_blueprint.doc(summary="Add new keyword", description="Adds new keyword for current user")
|
||||
def add_keyword(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_keyword_data_exists(data)
|
||||
|
||||
key = data['keyword']
|
||||
|
||||
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:
|
||||
abort(500, message="Keyword already exist for this user")
|
||||
|
||||
|
||||
@keyword_blueprint.route('/keyword', methods=['DELETE'])
|
||||
@keyword_blueprint.output(DeleteSuccessfulSchema, 200)
|
||||
@keyword_blueprint.input(schema=KeywordSchema)
|
||||
@keyword_blueprint.auth_required(auth)
|
||||
@keyword_blueprint.doc(summary="Removes existing keyword", description="Removes existing keyword for current user")
|
||||
def remove_keyword(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_keyword_data_exists(data)
|
||||
|
||||
key = data['keyword']
|
||||
|
||||
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", "data": {}})
|
||||
|
||||
|
||||
@keyword_blueprint.route('/keywords', methods=['GET'])
|
||||
@keyword_blueprint.output(KeywordResponseSchema(many=True), 200)
|
||||
@keyword_blueprint.auth_required(auth)
|
||||
@keyword_blueprint.doc(summary="Returns all keywords", description="Returns all keywords for current user")
|
||||
def get_keywords():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
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})
|
||||
|
||||
|
||||
def check_if_keyword_data_exists(data):
|
||||
if "keyword" not in data:
|
||||
abort(400, message="Keyword missing")
|
||||
|
||||
if data['keyword'] == "" or data['keyword'] is None:
|
||||
abort(400, message="Keyword missing")
|
33
api/api_blueprint_portfolio.py
Normal file
33
api/api_blueprint_portfolio.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from models import Transaction
|
||||
from auth import auth
|
||||
|
||||
portfolio_blueprint = APIBlueprint('portfolio', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@portfolio_blueprint.route('/portfolio', methods=['GET'])
|
||||
@portfolio_blueprint.output(200)
|
||||
@portfolio_blueprint.auth_required(auth)
|
||||
@portfolio_blueprint.doc(summary="Returns portfolio", description="Returns all shares of current user")
|
||||
def get_portfolio():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
return_portfolio = {}
|
||||
transactions = db.session.query(Transaction).filter_by(user_id=get_user_id_from_username(username)).all()
|
||||
|
||||
if transactions is not None:
|
||||
for row in transactions:
|
||||
if row.symbol in return_portfolio:
|
||||
return_portfolio[row.symbol]['count'] += row.count
|
||||
return_portfolio[row.symbol]['last_transaction'] = row.time
|
||||
else:
|
||||
return_portfolio[row.symbol] = {"count": row.count, "last_transaction": row.time}
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully loaded symbols", "data": return_portfolio})
|
83
api/api_blueprint_shares.py
Normal file
83
api/api_blueprint_shares.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint, abort
|
||||
from flask import jsonify
|
||||
|
||||
from auth import auth
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from models import Share
|
||||
from scheme import SymbolSchema, SymbolResponseSchema, DeleteSuccessfulSchema
|
||||
|
||||
shares_blueprint = APIBlueprint('share', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@shares_blueprint.route('/share', methods=['POST'])
|
||||
@shares_blueprint.output(SymbolResponseSchema(many=True), 200)
|
||||
@shares_blueprint.input(schema=SymbolSchema)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Add new symbol", description="Adds new symbol for current user")
|
||||
def add_symbol(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_symbol_data_exists(data)
|
||||
|
||||
symbol = data['symbol']
|
||||
|
||||
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'])
|
||||
@shares_blueprint.output(DeleteSuccessfulSchema, 200)
|
||||
@shares_blueprint.input(schema=SymbolSchema)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Removes existing symbol", description="Removes existing symbol for current user")
|
||||
def remove_symbol(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_symbol_data_exists(data)
|
||||
|
||||
symbol = data['symbol']
|
||||
|
||||
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", "data": {}})
|
||||
|
||||
|
||||
@shares_blueprint.route('/shares', methods=['GET'])
|
||||
@shares_blueprint.output(SymbolResponseSchema(many=True), 200)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Returns all symbols", description="Returns all symbols for current user")
|
||||
def get_symbol():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
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})
|
||||
|
||||
|
||||
def check_if_symbol_data_exists(data):
|
||||
if "symbol" not in data:
|
||||
abort(400, message="Symbol missing")
|
||||
|
||||
if data['symbol'] == "" or data['symbol'] is None:
|
||||
abort(400, message="Symbol missing")
|
80
api/api_blueprint_transactions.py
Normal file
80
api/api_blueprint_transactions.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
import datetime
|
||||
|
||||
from apiflask import abort, APIBlueprint
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from models import Transaction
|
||||
from scheme import TransactionSchema
|
||||
from auth import auth
|
||||
|
||||
transaction_blueprint = APIBlueprint('transaction', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@transaction_blueprint.route('/transaction', methods=['POST'])
|
||||
@transaction_blueprint.output((), 200)
|
||||
@transaction_blueprint.input(schema=TransactionSchema)
|
||||
@transaction_blueprint.auth_required(auth)
|
||||
@transaction_blueprint.doc(summary="Adds new transaction", description="Adds new transaction for current user")
|
||||
def add_transaction(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_transaction_data_exists(data)
|
||||
|
||||
new_transaction = Transaction(
|
||||
user_id=get_user_id_from_username(username),
|
||||
symbol=data['symbol'],
|
||||
time=datetime.datetime.strptime(data['time'], '%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
count=data['count'],
|
||||
price=data['price']
|
||||
)
|
||||
db.session.add(new_transaction)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully added transaction", "data": new_transaction.as_dict()})
|
||||
|
||||
|
||||
@transaction_blueprint.route('/transactions', methods=['GET'])
|
||||
@transaction_blueprint.output(TransactionSchema(), 200)
|
||||
@transaction_blueprint.auth_required(auth)
|
||||
@transaction_blueprint.doc(summary="Returns all transactions", description="Returns all transactions for current user")
|
||||
def get_transaction():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
return_transactions = []
|
||||
transactions = db.session.query(Transaction).filter_by(user_id=get_user_id_from_username(username)).all()
|
||||
|
||||
if transactions is not None:
|
||||
for row in transactions:
|
||||
return_transactions.append(row.as_dict())
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully loaded transactions", "data": return_transactions})
|
||||
|
||||
|
||||
def check_if_transaction_data_exists(data):
|
||||
if "symbol" not in data:
|
||||
abort(400, message="Symbol missing")
|
||||
|
||||
if data['symbol'] == "" or data['symbol'] is None:
|
||||
abort(400, message="Symbol missing")
|
||||
|
||||
if "time" not in data:
|
||||
abort(400, message="Time missing")
|
||||
|
||||
if data['time'] == "" or data['time'] is None:
|
||||
abort(400, message="Time missing")
|
||||
|
||||
if "count" not in data:
|
||||
abort(400, message="Count missing")
|
||||
|
||||
if data['count'] == "" or data['count'] is None:
|
||||
abort(400, message="Count missing")
|
||||
|
||||
if "price" not in data:
|
||||
abort(400, message="Price missing")
|
||||
|
||||
if data['price'] == "" or data['price'] is None:
|
||||
abort(400, message="Price missing")
|
199
api/api_blueprint_user.py
Normal file
199
api/api_blueprint_user.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import jwt
|
||||
from apiflask import APIBlueprint, abort
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import check_password, hash_password, get_username_or_abort_401, abort_if_no_admin
|
||||
from models import User
|
||||
from scheme import UsersSchema, TokenSchema, LoginDataSchema, AdminDataSchema, DeleteUserSchema
|
||||
from auth import auth
|
||||
|
||||
users_blueprint = APIBlueprint('users', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@users_blueprint.route('/users', methods=['GET'])
|
||||
@users_blueprint.output(UsersSchema(many=True), 200)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Get all users", description="Returns all existing users as array")
|
||||
def users():
|
||||
abort_if_no_admin()
|
||||
|
||||
res = []
|
||||
for i in User.query.all():
|
||||
res.append(i.as_dict())
|
||||
|
||||
return jsonify({"status": 200, "data": res})
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['GET'])
|
||||
@users_blueprint.output(UsersSchema(), 200)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Get current user", description="Returns current user")
|
||||
def user():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
res = db.session.query(User).filter_by(username=username).first().as_dict()
|
||||
|
||||
return jsonify({"status": 200, "data": res})
|
||||
|
||||
|
||||
@users_blueprint.route('/user/login', methods=['POST'])
|
||||
@users_blueprint.output(TokenSchema(), 200)
|
||||
@users_blueprint.input(schema=LoginDataSchema)
|
||||
@users_blueprint.doc(summary="Login", description="Returns jwt token if username and password match, otherwise returns error")
|
||||
def login(data):
|
||||
check_if_user_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is None: # Username doesn't exist
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
if not check_password(query_user.password, password): # Password incorrect
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
token = jwt.encode({'username': query_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": token}})
|
||||
|
||||
|
||||
@users_blueprint.route('/user/register', methods=['POST'])
|
||||
@users_blueprint.output(UsersSchema(), 200)
|
||||
@users_blueprint.input(schema=LoginDataSchema)
|
||||
@users_blueprint.doc(summary="Register", description="Registers user")
|
||||
def register(data):
|
||||
check_if_user_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is not None: # Username already exist
|
||||
abort(500, message="Username already exist")
|
||||
|
||||
new_user = User(
|
||||
username=username,
|
||||
password=hash_password(password),
|
||||
admin=False
|
||||
)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully registered user", "data": new_user.as_dict()})
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['PUT'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=LoginDataSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Update user", description="Changes password and/or username of current user")
|
||||
def update_user(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_user_data_exists(data)
|
||||
|
||||
new_username = data['username']
|
||||
new_password = data['password']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is None: # Username doesn't exist
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
if new_password is not None:
|
||||
query_user.password = hash_password(new_password)
|
||||
if new_username is not None:
|
||||
query_user.username = new_username
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully updated user", "data": {}})
|
||||
|
||||
|
||||
@users_blueprint.route('/user/setAdmin', methods=['PUT'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=AdminDataSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Set user admin state", description="Set admin state of specified user")
|
||||
def set_admin(data):
|
||||
abort_if_no_admin() # Only admin users can do this
|
||||
|
||||
check_if_admin_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
admin = data['admin']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is None: # Username doesn't exist
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
query_user.admin = admin
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully updated users admin rights", "data": {}})
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['DELETE'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=DeleteUserSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Delete user", description="Deletes user by username")
|
||||
def delete_user(data):
|
||||
check_if_delete_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
|
||||
if username == get_username_or_abort_401(): # Username is same as current user
|
||||
db.session.query(User).filter_by(username=username).delete()
|
||||
db.session.commit()
|
||||
else: # Delete different user than my user -> only admin users
|
||||
abort_if_no_admin()
|
||||
|
||||
db.session.query(User).filter_by(username=username).delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully removed user", "data": {}})
|
||||
|
||||
|
||||
def check_if_user_data_exists(data):
|
||||
if "username" not in data:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if data['username'] == "" or data['username'] is None:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if "password" not in data:
|
||||
abort(400, message="Password missing")
|
||||
|
||||
if data['password'] == "" or data['password'] is None:
|
||||
abort(400, message="Password missing")
|
||||
|
||||
|
||||
def check_if_admin_data_exists(data):
|
||||
if "username" not in data:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if data['username'] == "" or data['username'] is None:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if "admin" not in data:
|
||||
abort(400, message="Admin state missing")
|
||||
|
||||
if data['admin'] == "" or data['admin'] is None:
|
||||
abort(400, message="Admin state missing")
|
||||
|
||||
|
||||
def check_if_delete_data_exists(data):
|
||||
if "username" not in data:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if data['username'] == "" or data['username'] is None:
|
||||
abort(400, message="Username missing")
|
44
api/app.py
Normal file
44
api/app.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from apiflask import APIFlask
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from flask_cors import CORS
|
||||
|
||||
from models import *
|
||||
from api_blueprint_keyword import keyword_blueprint
|
||||
from api_blueprint_shares import shares_blueprint
|
||||
from api_blueprint_user import users_blueprint
|
||||
from api_blueprint_transactions import transaction_blueprint
|
||||
from api_blueprint_portfolio import portfolio_blueprint
|
||||
|
||||
|
||||
def create_app():
|
||||
load_dotenv()
|
||||
|
||||
# Create Flask app load app.config
|
||||
application = APIFlask(__name__)
|
||||
application.config.from_object("config.ConfigClass")
|
||||
|
||||
CORS(application)
|
||||
|
||||
application.app_context().push()
|
||||
|
||||
db.init_app(application)
|
||||
|
||||
# Create all tables
|
||||
db.create_all()
|
||||
|
||||
# api blueprints
|
||||
application.register_blueprint(keyword_blueprint)
|
||||
application.register_blueprint(shares_blueprint)
|
||||
application.register_blueprint(transaction_blueprint)
|
||||
application.register_blueprint(portfolio_blueprint)
|
||||
application.register_blueprint(users_blueprint)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
# Start development web server
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
18
api/auth.py
Normal file
18
api/auth.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
|
||||
import jwt
|
||||
from apiflask import HTTPTokenAuth
|
||||
|
||||
auth = HTTPTokenAuth()
|
||||
|
||||
|
||||
@auth.verify_token
|
||||
def verify_token(token):
|
||||
if token is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
jwt.decode(token, os.getenv('SECRET_KEY'), algorithms=["HS256"])
|
||||
return True
|
||||
except jwt.exceptions.DecodeError:
|
||||
return False
|
52
api/config.py
Normal file
52
api/config.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from scheme import BaseResponseSchema
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class ConfigClass(object):
|
||||
""" Flask application config """
|
||||
|
||||
# Flask settings
|
||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||
|
||||
# Flask-SQLAlchemy settings
|
||||
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + \
|
||||
os.getenv('MYSQL_USER') + ":" + \
|
||||
os.getenv('MYSQL_PASSWORD') + "@" + \
|
||||
os.getenv('MYSQL_HOST') + ":" + \
|
||||
(os.getenv("MYSQL_PORT") or str(3306)) + "/" + \
|
||||
os.getenv('MYSQL_DATABASE')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning
|
||||
|
||||
# openapi/Swagger config
|
||||
SPEC_FORMAT = 'yaml'
|
||||
SERVERS = [
|
||||
{
|
||||
"name": "Production",
|
||||
"url": "https://aktienbot.flokaiser.com"
|
||||
},
|
||||
{
|
||||
"name": "Local",
|
||||
"url": "http://127.0.0.1:5000"
|
||||
}
|
||||
]
|
||||
INFO = {
|
||||
'description': 'Webengineering 2 | Telegram Aktienbot',
|
||||
'version': '0.0.1'
|
||||
# 'termsOfService': 'http://example.com',
|
||||
# 'contact': {
|
||||
# 'name': 'API Support',
|
||||
# 'url': 'http://www.example.com/support',
|
||||
# 'email': 'support@example.com'
|
||||
# },
|
||||
# 'license': {
|
||||
# 'name': 'Apache 2.0',
|
||||
# 'url': 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||
# }
|
||||
}
|
||||
BASE_RESPONSE_DATA_KEY = "data"
|
||||
BASE_RESPONSE_SCHEMA = BaseResponseSchema
|
3
api/db.py
Normal file
3
api/db.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
2
api/deploy/healthcheck.sh
Normal file
2
api/deploy/healthcheck.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
curl -s http://localhost:80/ -o /dev/null || exit 1
|
41
api/deploy/nginx.conf
Normal file
41
api/deploy/nginx.conf
Normal file
@@ -0,0 +1,41 @@
|
||||
user root;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stdout;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
index index.html index.htm;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name homeserver.flokaiser.com;
|
||||
root /var/www/html;
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/tmp/uwsgi.socket;
|
||||
uwsgi_read_timeout 1h;
|
||||
uwsgi_send_timeout 1h;
|
||||
proxy_send_timeout 1h;
|
||||
proxy_read_timeout 1h;
|
||||
}
|
||||
}
|
||||
}
|
3
api/deploy/start.sh
Normal file
3
api/deploy/start.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
nginx -g "daemon off;" &
|
||||
uwsgi --ini deploy/uwsgi.ini
|
12
api/deploy/uwsgi.ini
Normal file
12
api/deploy/uwsgi.ini
Normal file
@@ -0,0 +1,12 @@
|
||||
[uwsgi]
|
||||
module = app:app
|
||||
uid = root
|
||||
gid = root
|
||||
master = true
|
||||
processes = 5
|
||||
|
||||
socket = /tmp/uwsgi.socket
|
||||
chmod-sock = 664
|
||||
vacuum = true
|
||||
|
||||
die-on-term = true
|
73
api/helper_functions.py
Normal file
73
api/helper_functions.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import hashlib
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import jwt
|
||||
from apiflask import abort
|
||||
from flask import request
|
||||
|
||||
from db import db
|
||||
from models import User
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
salt = uuid.uuid4().hex
|
||||
return hashlib.sha256(salt.encode() + password.encode()).hexdigest() + ':' + salt
|
||||
|
||||
|
||||
def check_password(hashed_password, user_password):
|
||||
password, salt = hashed_password.split(':')
|
||||
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 jwt.exceptions.DecodeError:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_username_from_token_data(token_data):
|
||||
if token_data is not None:
|
||||
return token_data['username']
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_user_id_from_username(username):
|
||||
if username is not None:
|
||||
return db.session.query(User).filter_by(username=username).first().user_id
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_username_or_abort_401():
|
||||
# get username from jwt token
|
||||
username = get_username_from_token_data(extract_token_data(get_token()))
|
||||
|
||||
if username is None: # If token not provided or invalid -> return 401 code
|
||||
abort(401, message="Unable to login")
|
||||
|
||||
return username
|
||||
|
||||
|
||||
def abort_if_no_admin():
|
||||
if not is_user_admin():
|
||||
abort(401, message="Only admin users can access this")
|
||||
|
||||
|
||||
def is_user_admin():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
return db.session.query(User).filter_by(username=username).first().admin
|
46
api/models.py
Normal file
46
api/models.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from db import db
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
username = db.Column('username', db.String(255), nullable=False, unique=True)
|
||||
password = db.Column('password', db.String(255), nullable=False, server_default='')
|
||||
user_id = db.Column('user_id', db.Integer(), primary_key=True)
|
||||
telegram_name = db.Column('telegram_name', db.String(255), nullable=True, server_default='')
|
||||
admin = db.Column('admin', db.Boolean(), server_default='0')
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
t_id = db.Column('t_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
user_id = db.Column('user_id', db.Integer(), db.ForeignKey('users.user_id', ondelete='CASCADE'))
|
||||
symbol = db.Column('symbol', db.String(255))
|
||||
time = db.Column('time', db.DateTime())
|
||||
count = db.Column('count', db.Integer())
|
||||
price = db.Column('price', db.Float())
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Keyword(db.Model):
|
||||
__tablename__ = 'keywords'
|
||||
s_id = db.Column('s_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
user_id = db.Column('user_id', db.Integer(), db.ForeignKey('users.user_id', ondelete='CASCADE'))
|
||||
keyword = db.Column('keyword', db.String(255))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Share(db.Model):
|
||||
__tablename__ = 'shares'
|
||||
a_id = db.Column('a_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
user_id = db.Column('user_id', db.Integer(), db.ForeignKey('users.user_id', ondelete='CASCADE'))
|
||||
symbol = db.Column('symbol', db.String(255))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
12
api/requirements.txt
Normal file
12
api/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
Flask~=2.0.3
|
||||
python-dotenv==0.19.2
|
||||
requests==2.27.1
|
||||
uwsgi==2.0.20
|
||||
Flask_SQLAlchemy==2.5.1
|
||||
python-dotenv==0.19.2
|
||||
pymysql==1.0.2
|
||||
pyjwt==2.0.0
|
||||
apiflask==0.12.0
|
||||
flask-swagger-ui==3.36.0
|
||||
flask-cors==3.0.10
|
||||
|
80
api/scheme.py
Normal file
80
api/scheme.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from apiflask import Schema
|
||||
from apiflask.fields import Integer, String, Boolean, Field, Float
|
||||
|
||||
|
||||
class BaseResponseSchema(Schema):
|
||||
text = String()
|
||||
status = Integer()
|
||||
data = Field()
|
||||
|
||||
|
||||
class UsersSchema(Schema):
|
||||
admin = Boolean()
|
||||
password = String()
|
||||
telegram_name = String()
|
||||
user_id = Integer()
|
||||
username = String()
|
||||
|
||||
|
||||
class AdminDataSchema(Schema):
|
||||
username = String()
|
||||
admin = Boolean()
|
||||
|
||||
|
||||
class TokenSchema(Schema):
|
||||
token = String()
|
||||
|
||||
|
||||
class LoginDataSchema(Schema):
|
||||
username = String()
|
||||
password = String()
|
||||
|
||||
|
||||
class DeleteUserSchema(Schema):
|
||||
username = String()
|
||||
|
||||
|
||||
class ChangePasswordSchema(Schema):
|
||||
old_password = String()
|
||||
new_password = String()
|
||||
|
||||
|
||||
class ChangeUsernameSchema(Schema):
|
||||
new_username = String()
|
||||
|
||||
|
||||
class KeywordSchema(Schema):
|
||||
keyword = String()
|
||||
|
||||
|
||||
class KeywordResponseSchema(Schema):
|
||||
keyword = String()
|
||||
s_id = Integer()
|
||||
user_id = Integer()
|
||||
|
||||
|
||||
class SymbolSchema(Schema):
|
||||
symbol = String()
|
||||
|
||||
|
||||
class SymbolResponseSchema(Schema):
|
||||
symbol = String()
|
||||
s_id = Integer()
|
||||
user_id = Integer()
|
||||
|
||||
|
||||
class PortfolioShareResponseSchema(Schema):
|
||||
count = Integer()
|
||||
last_transaction = String()
|
||||
|
||||
|
||||
class TransactionSchema(Schema):
|
||||
user_id = Integer()
|
||||
symbol = String()
|
||||
time = String()
|
||||
count = Integer()
|
||||
price = Float()
|
||||
|
||||
|
||||
class DeleteSuccessfulSchema(Schema):
|
||||
pass
|
Reference in New Issue
Block a user