diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 62eaf5b..0000000 --- a/.drone.yml +++ /dev/null @@ -1,77 +0,0 @@ -kind: pipeline -type: docker -name: linux-amd64 - -platform: - arch: amd64 - os: linux - -steps: -- name: generate_tag - image: golang - commands: - - echo -n "${DRONE_BRANCH//\//-}-${DRONE_COMMIT_SHA:0:8}, latest" > .tags - - -- name: publish_api - image: plugins/docker - settings: - username: - from_secret: username - password: - from_secret: password - registry: - from_secret: registry - repo: - from_secret: repo_api - dockerfile: api/Dockerfile - -- name: publish_frontend - image: plugins/docker - settings: - username: - from_secret: username - password: - from_secret: password - registry: - from_secret: registry - repo: - from_secret: repo_frontend - dockerfile: frontend/Dockerfile - -- name: publish_bot - image: plugins/docker - settings: - username: - from_secret: username - password: - from_secret: password - registry: - from_secret: registry - repo: - from_secret: repo_bot - dockerfile: telegram_bot/Dockerfile - -- name: deploy - image: appleboy/drone-ssh - network_mode: host - environment: - REPO: - from_secret: repo - IP: - from_secret: deploy_ip - NET: - from_secret: deploy_net - NAME: - from_secret: deploy_name - PLUGIN_HOST: - from_secret: ssh_host - PLUGIN_USERNAME: - from_secret: ssh_user - PLUGIN_PASSWORD: - from_secret: ssh_password - PLUGIN_SCRIPT: /opt/docker/TelegramAktienBot/deploy.sh - -trigger: - branch: - - main diff --git a/.env.example b/.env.example deleted file mode 100644 index 921ca00..0000000 --- a/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# MYSQL Database -MYSQL_HOST= -MYSQL_PORT= -MYSQL_DATABASE= -MYSQL_USER= -MYSQL_PASSWORD= - -# Telegram bot api key -BOT_API_KEY="" -NEWS_API_KEY="" - -# Flask secret key -SECRET_KEY= diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..76ca8b1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,35 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # API + - package-ecosystem: "pip" + directory: "/api" + schedule: + interval: "daily" + assignees: + - "H4CK3R-01" + open-pull-requests-limit: 100 + + # Bot + - package-ecosystem: "pip" + directory: "/telegram_bot" + schedule: + interval: "daily" + assignees: + - "NormalParameter" + - "Rripped" + - "FlorianKellermann" + open-pull-requests-limit: 100 + + # Frontend + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "daily" + assignees: + - "kevinpauer" + open-pull-requests-limit: 100 diff --git a/.gitignore b/.gitignore index fba0141..ac68c97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .idea/* .env -.env.example env Lib Include diff --git a/.woodpecker/pipeline.yml b/.woodpecker/pipeline.yml new file mode 100644 index 0000000..7a0d6a1 --- /dev/null +++ b/.woodpecker/pipeline.yml @@ -0,0 +1,87 @@ +pipeline: + generate_docker_tag: + image: golang + commands: + - echo -n "${CI_COMMIT_BRANCH//\//-}-${CI_COMMIT_SHA:0:8}, latest" > .tags + when: + path: [ "frontend/**", "telegram_bot/**", "api/**" ] + event: push + + + # -------------------------------------- API -------------------------------------- + build_api: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: + from_secret: repo_api + username: + from_secret: username + password: + from_secret: password + registry: + from_secret: registry + dockerfile: api/Dockerfile + platforms: linux/amd64 + when: + path: "api/**" + event: push + + + # -------------------------------------- Bot -------------------------------------- + build_bot: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: + from_secret: repo_bot + username: + from_secret: username + password: + from_secret: password + registry: + from_secret: registry + dockerfile: telegram_bot/Dockerfile + platforms: linux/amd64 + when: + path: "telegram_bot/**" + event: push + + + # -------------------------------------- Frontend -------------------------------------- + build_frontend: + image: woodpeckerci/plugin-docker-buildx + settings: + repo: + from_secret: repo_frontend + username: + from_secret: username + password: + from_secret: password + registry: + from_secret: registry + dockerfile: frontend/Dockerfile + platforms: linux/amd64 + when: + path: "frontend/**" + event: push + + + # -------------------------------------- Deploy -------------------------------------- + deploy: + image: appleboy/drone-ssh + network_mode: host + settings: + host: + from_secret: ssh_host + username: + from_secret: ssh_user + password: + from_secret: ssh_password + script: + - cd /root/docker/aktienbot + - docker-compose pull + - docker-compose -p "aktienbot" up -d + when: + path: [ "frontend/**", "telegram_bot/**", "api/**" ] + event: push + +branches: main diff --git a/README.md b/README.md index 96a70d7..0bb8abd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://drone.flokaiser.com/api/badges/H4CK3R-01/TelegramAktienBot/status.svg?ref=main)](https://drone.flokaiser.com/H4CK3R-01/TelegramAktienBot) +[![Build Status](https://woodpecker.flokaiser.com/api/badges/WebEngineering2/TelegramAktienBot/status.svg)](https://woodpecker.flokaiser.com/WebEngineering2/TelegramAktienBot/) # TelegramAktienBot WebEngineering2 Projekt: Aktien und News Bot für Telegram @@ -15,12 +15,22 @@ WebEngineering2 Projekt: Aktien und News Bot für Telegram - Update auf Anfrage ## Dokumentation -- Postman-API -> docs/postman.json -- Datenbank -> database/* +-> README.md in /documentation -## Local setup for telegram bot -0. optional: build virtual env by ``python -m venv venv`` - ``env/Scripts/activate`` -2. create .env and set API keys etc. (use .env.example as a layout) -3. install required libs via ``pip install -r ./telegram_bot/requirements.txt`` -4. run bot.py via ``python ./telegram_bot/bot.py`` +## Team +* Florian Kaiser +* Florian Kellermann +* Linus Eickhoff +* Kevin Pauer + +## Nützliche Tools +- Portainer (https://gruppe1.testsites.info/portainer/) \ + *Container Management System* +- phpMyAdmin (https://gruppe1.testsites.info/phpmyadmin/) \ + *Administration von MySQL-Datenbanken* +- goaccess (https://gruppe1.testsites.info/goaccess/) \ + *Webanalyseanwendung* +- Uptimekuma (https://uptimekuma.flokaiser.com/status/aktienbot) \ + *Monitoring* +- Woodpecker (https://woodpecker.flokaiser.com/WebEngineering2/TelegramAktienBot) \ + *Continuous Integration platform* diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..2d183f9 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,21 @@ +# MYSQL Database +MYSQL_HOST= +MYSQL_PORT= +MYSQL_DATABASE= +MYSQL_USER= +MYSQL_PASSWORD= + +# Flask secret key +SECRET_KEY= + +# Users +BOT_EMAIL= +BOT_USERNAME= +BOT_PASSWORD= + +ADMIN_EMAIL= +ADMIN_USERNAME= +ADMIN_PASSWORD= + +# API URL (used for load_share_price.py and generate_sample_transactions.py) +API_URL= \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 2736a90..c9a163b 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,19 +1,28 @@ -FROM python:3.10-alpine +FROM python:3.10-slim +# Change the working directory to the root of the project WORKDIR /srv/flask_app -RUN apk add nginx build-base libffi-dev curl uwsgi +# Install dependencies +RUN apt update && apt install -y python3 python3-pip curl nginx && rm -rf /var/lib/apt/lists/* + +# Install the dependencies COPY api/requirements.txt /srv/flask_app/ - RUN pip install -r requirements.txt --src /usr/local/src --no-warn-script-location +# Copy the source code to the working directory COPY api /srv/flask_app COPY api/deploy/nginx.conf /etc/nginx + +# Change file permissions RUN chmod +x ./deploy/start.sh RUN chmod +x ./deploy/healthcheck.sh +# Set healthcheck HEALTHCHECK --interval=15s --timeout=2s CMD ["./deploy/healthcheck.sh"] +# Expose webserver port EXPOSE 80 +# Run the app CMD ["./deploy/start.sh"] diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..e9daaa2 --- /dev/null +++ b/api/README.md @@ -0,0 +1,51 @@ +# API + +Aktienbot API + +## Development +1. Create virtual environment `python -m venv venv env/Scripts/activate` +2. Install requirements `pip install -r api/requirements.txt` +3. Set environment variables (see list below) + 1. Use `.env`-file in `api` directory like `.env.example` + 2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`) +4. Run api `python api/app.py` + + +## Testing +1. Create virtual environment `python -m venv venv env/Scripts/activate` +2. Install requirements `pip install -r api/requirements.txt` +3. Set environment variables (see list below) + 1. Use `.env`-file in `api` directory like `.env.example` + 2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`) +4. Change directory: `cd api/` +5. Run tests: `python -m pytest -v --cov-report term-missing --cov=app` + +## Environment variables +``` + # Flask secret key + SECRET_KEY= + + # MYSQL Connection + MYSQL_USER= + MYSQL_PASSWORD= + MYSQL_HOST= + MYSQL_PORT= + MYSQL_DATABASE= +``` + +## Docker +``` +docker run -d \ + --name aktienbot_api \ + --hostname aktienbot_api \ + --publish 80:80 \ + --env "SECRET_KEY=" \ + --env "MYSQL_USER=" \ + --env "MYSQL_PASSWORD=" \ + --env "MYSQL_HOST=" \ + --env "MYSQL_PORT=" \ + --env "MYSQL_DATABASE=" \ + --restart unless-stopped \ + registry.flokaiser.com/aktienbot/api:latest +``` +or load environment variables from file by using `--env-file ` \ No newline at end of file diff --git a/api/api_blueprint_portfolio.py b/api/api_blueprint_portfolio.py deleted file mode 100644 index 2a54275..0000000 --- a/api/api_blueprint_portfolio.py +++ /dev/null @@ -1,33 +0,0 @@ -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}) diff --git a/api/api_blueprint_shares.py b/api/api_blueprint_shares.py deleted file mode 100644 index ef68d99..0000000 --- a/api/api_blueprint_shares.py +++ /dev/null @@ -1,83 +0,0 @@ -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") diff --git a/api/api_blueprint_transactions.py b/api/api_blueprint_transactions.py deleted file mode 100644 index 2a8fed2..0000000 --- a/api/api_blueprint_transactions.py +++ /dev/null @@ -1,80 +0,0 @@ -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") diff --git a/api/api_blueprint_user.py b/api/api_blueprint_user.py deleted file mode 100644 index c7e6940..0000000 --- a/api/api_blueprint_user.py +++ /dev/null @@ -1,199 +0,0 @@ -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") diff --git a/api/app.py b/api/app.py index 88049f6..28fe3e9 100644 --- a/api/app.py +++ b/api/app.py @@ -1,44 +1,11 @@ -from apiflask import APIFlask +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" -from dotenv import load_dotenv -from flask_cors import CORS +from app import create_app -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__, docs_path='/api/docs') - 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() +# Create an application instance that web servers can use. +application = create_app('config/flask.cfg') +application.run() diff --git a/api/app/__init__.py b/api/app/__init__.py new file mode 100644 index 0000000..30822d3 --- /dev/null +++ b/api/app/__init__.py @@ -0,0 +1,73 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +from flask import current_app +from apiflask import APIFlask + +from dotenv import load_dotenv +from flask_cors import CORS + +from app.blueprints.keyword import keyword_blueprint +from app.blueprints.portfolio import portfolio_blueprint +from app.blueprints.shares import shares_blueprint +from app.blueprints.share_price import share_price_blueprint +from app.blueprints.transactions import transaction_blueprint +from app.blueprints.telegram import telegram_blueprint +from app.blueprints.user import users_blueprint +from app.helper_functions import hash_password +from app.models import * + + +def create_app(config_filename=None): + load_dotenv() + + # Create Flask app load app.config + application = APIFlask(__name__, openapi_blueprint_url_prefix='/api') + application.config.from_pyfile(config_filename) + + CORS(application, resources={r"*": {"origins": "*"}}) + + db.init_app(application) + + # api blueprints + application.register_blueprint(keyword_blueprint) + application.register_blueprint(shares_blueprint) + application.register_blueprint(share_price_blueprint) + application.register_blueprint(transaction_blueprint) + application.register_blueprint(portfolio_blueprint) + application.register_blueprint(users_blueprint) + application.register_blueprint(telegram_blueprint) + + @application.before_first_request + def init_database(): + db.create_all() + + if current_app.config['BOT_EMAIL'] is not None and current_app.config['BOT_USERNAME'] is not None and current_app.config['BOT_PASSWORD'] is not None: + if db.session.query(User).filter_by(email=current_app.config['BOT_EMAIL']).first() is None: # Check if user already exist + bot = User( + email=current_app.config['BOT_EMAIL'], + username=current_app.config['BOT_USERNAME'], + password=hash_password(current_app.config['BOT_PASSWORD']), + admin=False + ) + db.session.add(bot) + db.session.commit() + + if current_app.config['ADMIN_EMAIL'] is not None and current_app.config['ADMIN_USERNAME'] is not None and current_app.config['ADMIN_PASSWORD'] is not None: + if db.session.query(User).filter_by(email=current_app.config['ADMIN_EMAIL']).first() is None: # Check if user already exist + admin = User( + email=current_app.config['ADMIN_EMAIL'], + username=current_app.config['ADMIN_USERNAME'], + password=hash_password(current_app.config['ADMIN_PASSWORD']), + admin=True + ) + db.session.add(admin) + db.session.commit() + + return application + + +app = create_app("config/flask.cfg") diff --git a/api/app/auth.py b/api/app/auth.py new file mode 100644 index 0000000..e1a4bba --- /dev/null +++ b/api/app/auth.py @@ -0,0 +1,29 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +from flask import current_app + +import jwt +from apiflask import HTTPTokenAuth + +auth = HTTPTokenAuth() + + +@auth.verify_token +def verify_token(token): + if token is None: + return False + + # We decided to append the user id to the bearer token using ":" as separator to select an specific user + # To validate the token we can remove the user id since we only validate the token and not the user id + if ':' in token: + token = token.split(":")[0] + + try: + jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"]) + return True + except jwt.PyJWTError: + return False diff --git a/api/app/blueprints/__init__.py b/api/app/blueprints/__init__.py new file mode 100644 index 0000000..dfaac6b --- /dev/null +++ b/api/app/blueprints/__init__.py @@ -0,0 +1,5 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" diff --git a/api/api_blueprint_keyword.py b/api/app/blueprints/keyword.py similarity index 51% rename from api/api_blueprint_keyword.py rename to api/app/blueprints/keyword.py index 7474bcb..e3edc21 100644 --- a/api/api_blueprint_keyword.py +++ b/api/app/blueprints/keyword.py @@ -1,13 +1,18 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + 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 +from app.db import database as db +from app.helper_functions import make_response, get_email_or_abort_401 +from app.auth import auth +from app.schema import KeywordResponseSchema, KeywordSchema, DeleteSuccessfulSchema +from app.models import Keyword keyword_blueprint = APIBlueprint('keyword', __name__, url_prefix='/api') __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) @@ -19,23 +24,25 @@ __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file @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() + email = get_email_or_abort_401() - check_if_keyword_data_exists(data) + if not check_if_keyword_data_exists(data): + abort(400, message="Keyword missing") key = data['keyword'] - check_keyword = db.session.query(Keyword).filter_by(keyword=key, user_id=get_user_id_from_username(username)).first() + # Check if keyword already exists + check_keyword = db.session.query(Keyword).filter_by(keyword=key, email=email).first() if check_keyword is None: - # Keyword doesn't exist yet for this user + # Keyword doesn't exist yet for this user -> add it new_keyword = Keyword( - user_id=get_user_id_from_username(username), + email=email, keyword=key ) db.session.add(new_keyword) db.session.commit() - return jsonify({"status": 200, "text": "Successfully added keyword", "data": new_keyword.as_dict()}) + return make_response(new_keyword.as_dict(), 200, "Successfully added keyword") else: abort(500, message="Keyword already exist for this user") @@ -46,16 +53,22 @@ def add_keyword(data): @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() + email = get_email_or_abort_401() - check_if_keyword_data_exists(data) + # Check if request data is valid + if not check_if_keyword_data_exists(data): + abort(400, message="Keyword missing") - key = data['keyword'] + # Check if keyword exists + check_keyword = db.session.query(Keyword).filter_by(keyword=data['keyword'], email=email).first() + if check_keyword is None: + return abort(500, "Keyword doesn't exist for this user") + else: + # Keyword exists -> delete it + db.session.query(Keyword).filter_by(keyword=data['keyword'], email=email).delete() + db.session.commit() - 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": {}}) + return make_response({}, 200, "Successfully removed keyword") @keyword_blueprint.route('/keywords', methods=['GET']) @@ -63,21 +76,25 @@ def remove_keyword(data): @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() + email = get_email_or_abort_401() return_keywords = [] - keywords = db.session.query(Keyword).filter_by(user_id=get_user_id_from_username(username)).all() + keywords = db.session.query(Keyword).filter_by(email=email).all() + # If no keywords exist for this user -> return empty list + # Otherwise iterate over all keywords, convert them to json and add them to the return list 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}) + return make_response(return_keywords, 200, "Successfully loaded keywords") def check_if_keyword_data_exists(data): if "keyword" not in data: - abort(400, message="Keyword missing") + return False if data['keyword'] == "" or data['keyword'] is None: - abort(400, message="Keyword missing") + return False + + return True diff --git a/api/app/blueprints/portfolio.py b/api/app/blueprints/portfolio.py new file mode 100644 index 0000000..9ead66b --- /dev/null +++ b/api/app/blueprints/portfolio.py @@ -0,0 +1,52 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.1" + +import os + +from apiflask import APIBlueprint +from app.auth import auth +from app.db import database as db +from app.helper_functions import make_response, get_email_or_abort_401 +from app.models import SharePrice +from app.schema import PortfolioResponseSchema + +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(PortfolioResponseSchema(many=True), 200) +@portfolio_blueprint.auth_required(auth) +@portfolio_blueprint.doc(summary="Returns portfolio", description="Returns all shares of current user") +def get_portfolio(): + email = get_email_or_abort_401() + + return_portfolio = [] + + # Get all transactions of current user + transactions = db.session.execute("SELECT isin, comment, SUM(count), SUM(price), MAX(time) FROM `transactions` WHERE email = '" + email + "' GROUP BY isin, comment;").all() + + # If there are no transactions, return empty portfolio + # Otherwise calculate portfolio + if transactions is not None: + for row in transactions: + data = { + "isin": row[0], + "comment": row[1], + "count": row[2], + # "calculated_price": row[3], + "last_transaction": row[4], + 'current_price': 0 + } + + # Add current share value to portfolio + query_share_price = db.session.query(SharePrice).filter_by(isin=row[0]).order_by(SharePrice.date.desc()).first() + if query_share_price is not None: + data['current_price'] = query_share_price.as_dict()['price'] + + return_portfolio.append(data) + + return make_response(return_portfolio, 200, "Successfully loaded symbols") diff --git a/api/app/blueprints/share_price.py b/api/app/blueprints/share_price.py new file mode 100644 index 0000000..50f57ae --- /dev/null +++ b/api/app/blueprints/share_price.py @@ -0,0 +1,92 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.1" + +import datetime +import os + +from apiflask import APIBlueprint, abort +from app.auth import auth +from app.db import database as db +from app.helper_functions import make_response +from app.models import SharePrice +from app.schema import SymbolPriceSchema + +share_price_blueprint = APIBlueprint('share_price', __name__, url_prefix='/api') +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +@share_price_blueprint.route('/symbols', methods=['GET']) +@share_price_blueprint.output({}, 200) +@share_price_blueprint.auth_required(auth) +@share_price_blueprint.doc(summary="Returns all transaction symbols", description="Returns all transaction symbols for all users") +def get_transaction_symbols(): + # Get all transaction symbols + symbols = db.session.execute("SELECT isin FROM `transactions` GROUP BY isin;").all() + + return_symbols = [] + for s in symbols: + return_symbols.append(s[0]) + + return make_response(return_symbols, 200, "Successfully loaded symbols") + + +@share_price_blueprint.route('/symbol', methods=['POST']) +@share_price_blueprint.output({}, 200) +@share_price_blueprint.input(schema=SymbolPriceSchema) +@share_price_blueprint.auth_required(auth) +@share_price_blueprint.doc(summary="Adds new price for isin", description="Adds new price to database") +def add_symbol_price(data): + # Check if required data is available + if not check_if_isin_data_exists(data): + abort(400, message="ISIN missing") + + if not check_if_price_data_exists(data): + abort(400, message="Price missing") + + if not check_if_time_data_exists(data): + abort(400, message="Time missing") + + # Add share price + share_price = SharePrice( + isin=data['isin'], + price=data['price'], + date=datetime.datetime.strptime(data['time'], '%Y-%m-%dT%H:%M:%S.%fZ'), + ) + + db.session.add(share_price) + db.session.commit() + + return make_response(share_price.as_dict(), 200, "Successfully added price") + + +def check_if_isin_data_exists(data): + if 'isin' not in data: + return False + + if data['isin'] == "" or data['isin'] is None: + return False + + return True + + +def check_if_price_data_exists(data): + if 'price' not in data: + return False + + if data['price'] == "" or data['price'] is None: + return False + + return True + + +def check_if_time_data_exists(data): + if 'time' not in data: + return False + + if data['time'] == "" or data['time'] is None: + return False + + return True diff --git a/api/app/blueprints/shares.py b/api/app/blueprints/shares.py new file mode 100644 index 0000000..ad0ffe2 --- /dev/null +++ b/api/app/blueprints/shares.py @@ -0,0 +1,112 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.1" + +import os + +from apiflask import APIBlueprint, abort +from app.auth import auth +from app.db import database as db +from app.helper_functions import make_response, get_email_or_abort_401 +from app.models import Share +from app.schema import SymbolSchema, SymbolResponseSchema, DeleteSuccessfulSchema, SymbolRemoveSchema + +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): + email = get_email_or_abort_401() + + # Check if required data is available + if not check_if_isin_data_exists(data): + abort(400, message="ISIN missing") + + if not check_if_comment_data_exists(data): + abort(400, message="Comment missing") + + # Check if share already exists + check_share = db.session.query(Share).filter_by(isin=data['isin'], email=email).first() + if check_share is None: + # Keyword doesn't exist yet for this user -> add it + new_symbol = Share( + email=email, + isin=data['isin'], + comment=data['comment'] + ) + db.session.add(new_symbol) + db.session.commit() + + return make_response(new_symbol.as_dict(), 200, "Successfully added symbol") + else: + abort(500, "Symbol already exist for this user") + + +@shares_blueprint.route('/share', methods=['DELETE']) +@shares_blueprint.output(DeleteSuccessfulSchema, 200) +@shares_blueprint.input(schema=SymbolRemoveSchema) +@shares_blueprint.auth_required(auth) +@shares_blueprint.doc(summary="Removes existing symbol", description="Removes existing symbol for current user") +def remove_symbol(data): + email = get_email_or_abort_401() + + # Check if required data is available + if not check_if_isin_data_exists(data): + abort(400, message="ISIN missing") + + # Check if share exists + check_share = db.session.query(Share).filter_by(isin=data['isin'], email=email).first() + if check_share is None: + abort(500, "Symbol doesn't exist for this user") + else: + # Delete share + db.session.query(Share).filter_by(isin=data['isin'], email=email).delete() + db.session.commit() + + return make_response({}, 200, "Successfully removed symbol") + + +@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(): + email = get_email_or_abort_401() + + return_symbols = [] + symbols = db.session.query(Share).filter_by(email=email).all() + + # If no shares exist for this user -> return empty list + # Otherwise iterate over all shares, convert them to json and add them to the return list + if symbols is not None: + for row in symbols: + return_symbols.append(row.as_dict()) + + return make_response(return_symbols, 200, "Successfully loaded symbols") + + +def check_if_isin_data_exists(data): + if "isin" not in data: + return False + + if data['isin'] == "" or data['isin'] is None: + return False + + return True + + +def check_if_comment_data_exists(data): + if "comment" not in data: + return False + + if data['comment'] == "" or data['comment'] is None: + return False + + return True diff --git a/api/app/blueprints/telegram.py b/api/app/blueprints/telegram.py new file mode 100644 index 0000000..b492a91 --- /dev/null +++ b/api/app/blueprints/telegram.py @@ -0,0 +1,47 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import os + +from apiflask import APIBlueprint, abort +from app.auth import auth +from app.db import database as db +from app.helper_functions import make_response, get_email_or_abort_401, get_user +from app.schema import TelegramIdSchema, UsersSchema + +telegram_blueprint = APIBlueprint('telegram', __name__, url_prefix='/api') +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +@telegram_blueprint.route('/telegram', methods=['POST']) +@telegram_blueprint.output(UsersSchema(many=False), 200) +@telegram_blueprint.input(schema=TelegramIdSchema) +@telegram_blueprint.auth_required(auth) +@telegram_blueprint.doc(summary="Connects telegram user id", description="Connects telegram user id to user account") +def add_keyword(data): + email = get_email_or_abort_401() + + # Check if request data is valid + if not check_if_telegram_user_id_data_exists(data): + abort(400, message="User ID missing") + + query_user = get_user(email) + + # Change user id + query_user.telegram_user_id = data['telegram_user_id'] + db.session.commit() + + return make_response(query_user.as_dict(), 200, "Successfully connected telegram user") + + +def check_if_telegram_user_id_data_exists(data): + if "telegram_user_id" not in data: + return False + + if data['telegram_user_id'] == "" or data['telegram_user_id'] is None: + return False + + return True diff --git a/api/app/blueprints/transactions.py b/api/app/blueprints/transactions.py new file mode 100644 index 0000000..d34c799 --- /dev/null +++ b/api/app/blueprints/transactions.py @@ -0,0 +1,127 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.1" + +import datetime +import os + +from apiflask import abort, APIBlueprint +from app.auth import auth +from app.db import database as db +from app.helper_functions import make_response, get_email_or_abort_401 +from app.models import Transaction +from app.schema import TransactionSchema, TransactionResponseSchema + +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(TransactionResponseSchema(), 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): + email = get_email_or_abort_401() + + # Check if required data is available + if not check_if_isin_data_exists(data): + abort(400, "ISIN missing") + + if not check_if_time_data_exists(data): + abort(400, "Time missing") + + if not check_if_comment_data_exists(data): + abort(400, "Comment missing") + + if not check_if_count_data_exists(data): + abort(400, "Count missing") + + if not check_if_price_data_exists(data): + abort(400, "Price missing") + + # Add transaction + new_transaction = Transaction( + email=email, + isin=data['isin'], + comment=data['comment'], + 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 make_response(new_transaction.as_dict(), 200, "Successfully added transaction") + + +@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(): + email = get_email_or_abort_401() + + return_transactions = [] + + # Get all transactions + transactions = db.session.query(Transaction).filter_by(email=email).all() + + # Iterate over transactions and add them to return_transactions + if transactions is not None: + for row in transactions: + return_transactions.append(row.as_dict()) + + return make_response(return_transactions, 200, "Successfully loaded transactions") + + +def check_if_isin_data_exists(data): + if "isin" not in data: + return False + + if data['isin'] == "" or data['isin'] is None: + return False + + return True + + +def check_if_time_data_exists(data): + if "time" not in data: + return False + + if data['time'] == "" or data['time'] is None: + return False + + return True + + +def check_if_comment_data_exists(data): + if "comment" not in data: + return False + + if data['comment'] == "" or data['comment'] is None: + return False + + return True + + +def check_if_count_data_exists(data): + if "count" not in data: + return False + + if data['count'] == "" or data['count'] is None: + return False + + return True + + +def check_if_price_data_exists(data): + if "price" not in data: + return False + + if data['price'] == "" or data['price'] is None: + return False + + return True diff --git a/api/app/blueprints/user.py b/api/app/blueprints/user.py new file mode 100644 index 0000000..cc6b510 --- /dev/null +++ b/api/app/blueprints/user.py @@ -0,0 +1,258 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import datetime +import os + +import jwt +from apiflask import APIBlueprint, abort +from app.auth import auth +from app.db import database as db +from app.helper_functions import check_password, hash_password, abort_if_no_admin, make_response, get_email_or_abort_401, get_user +from app.models import User +from app.schema import UsersSchema, TokenSchema, LoginDataSchema, AdminDataSchema, DeleteUserSchema, RegisterDataSchema, UpdateUserDataSchema, CronDataSchema +from flask import current_app + +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 = [] + + # Query all users and convert them to dicts + for i in User.query.all(): + res.append(i.as_dict()) + + return make_response(res, 200, "Successfully received all users") + + +@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(): + email = get_email_or_abort_401() + + # Query current user + query_user = get_user(email) + + return make_response(query_user.as_dict(), 200, "Successfully received current user data") + + +@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 required data is available + if not check_if_password_data_exists(data): + abort(400, "Password missing") + + if not check_if_email_data_exists(data): + abort(400, "Email missing") + + # Query current user + query_user = get_user(data['email']) + + # Check if password matches + if not check_password(query_user.password, data['password'].encode("utf-8")): # Password incorrect + abort(500, message="Unable to login") + + # Check if user is bot + if query_user.email == current_app.config['BOT_EMAIL']: + # Set bot token valid for 1 year + token = jwt.encode({'email': query_user.email, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=365)}, current_app.config['SECRET_KEY'], "HS256") + else: + # Set token valid for 1 day + token = jwt.encode({'email': query_user.email, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1)}, current_app.config['SECRET_KEY'], "HS256") + + return make_response({"token": token}, 200, "Successfully logged in") + + +@users_blueprint.route('/user/register', methods=['POST']) +@users_blueprint.output(UsersSchema(), 200) +@users_blueprint.input(schema=RegisterDataSchema) +@users_blueprint.doc(summary="Register", description="Registers user") +def register(data): + # Check if required data is available + if not check_if_email_data_exists(data): + abort(400, "Email missing") + + if not check_if_username_data_exists(data): + abort(400, "Username missing") + + if not check_if_password_data_exists(data): + abort(400, "Password missing") + + # Check if user already exists + query_user = db.session.query(User).filter_by(email=data['email']).first() + if query_user is not None: + abort(500, message="Email already exist") + + # Add user to database + new_user = User( + email=data['email'], + username=data['username'], + password=hash_password(data['password']), + admin=False, + cron="0 8 * * *" + ) + db.session.add(new_user) + db.session.commit() + + return make_response(new_user.as_dict(), 200, "Successfully registered user") + + +@users_blueprint.route('/user', methods=['PUT']) +@users_blueprint.output({}, 200) +@users_blueprint.input(schema=UpdateUserDataSchema) +@users_blueprint.auth_required(auth) +@users_blueprint.doc(summary="Update user", description="Changes password and/or username of current user") +def update_user(data): + email = get_email_or_abort_401() + + # Query current user + query_user = get_user(email) + + # Check if password data is available -> if, change password + if check_if_password_data_exists(data): + query_user.password = hash_password(data['password']) + + # Check if username data is available -> if, change username + if check_if_username_data_exists(data): + query_user.username = data['username'] + + db.session.commit() + + return make_response({}, 200, "Successfully updated user") + + +@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 required data is available + if not check_if_email_data_exists(data): + abort(400, "Email missing") + + if not check_if_admin_data_exists(data): + abort(400, "Admin data missing") + + # Get user by email + query_user = get_user(data['email']) + + # Update user admin state + query_user.admin = data['admin'] + db.session.commit() + + return make_response({}, 200, "Successfully updated users admin rights") + + +@users_blueprint.route('/user/setCron', methods=['PUT']) +@users_blueprint.output({}, 200) +@users_blueprint.input(schema=CronDataSchema) +@users_blueprint.auth_required(auth) +@users_blueprint.doc(summary="Set update cron", description="Set update cron of specified user") +def set_cron(data): + email = get_email_or_abort_401() + + # Check if required data is available + if not check_if_cron_data_exists(data): + abort(400, "Cron data missing") + + # Update user cron + get_user(email).cron = data['cron'] + db.session.commit() + + return make_response({}, 200, "Successfully updated users cron") + + +@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 required data is available + if not check_if_email_data_exists(data): + abort(400, "Email missing") + + # Check if email to delete is current user + # -> if, delete user + # -> if not, check if user is admin + # -> if, delete user + # -> else, abort + if data['email'] == get_email_or_abort_401(): # Username is same as current user + db.session.query(User).filter_by(email=data['email']).delete() + db.session.commit() + else: + abort_if_no_admin() + + db.session.query(User).filter_by(email=data['email']).delete() + db.session.commit() + + return make_response({}, 200, "Successfully removed user") + + +def check_if_email_data_exists(data): + if "email" not in data: + return False + + if data['email'] == "" or data['email'] is None: + return False + + return True + + +def check_if_password_data_exists(data): + if "password" not in data: + return False + + if data['password'] == "" or data['password'] is None: + return False + + return True + + +def check_if_username_data_exists(data): + if "username" not in data: + return False + + if data['username'] == "" or data['username'] is None: + return False + + return True + + +def check_if_admin_data_exists(data): + if "admin" not in data: + return False + + if data['admin'] == "" or data['admin'] is None: + return False + + return True + + +def check_if_cron_data_exists(data): + if "cron" not in data: + return False + + if data['cron'] == "" or data['cron'] is None: + return False + + return True diff --git a/api/app/config/flask.cfg b/api/app/config/flask.cfg new file mode 100644 index 0000000..2fec222 --- /dev/null +++ b/api/app/config/flask.cfg @@ -0,0 +1,33 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import os +from app.schema import BaseResponseSchema + +# Flask settings +SECRET_KEY = os.getenv('SECRET_KEY', 'secret string') + + +# Flask-SQLAlchemy settings +SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.getenv('MYSQL_USER') + ":" + os.getenv('MYSQL_PASSWORD') + "@" + os.getenv('MYSQL_HOST') + ":" + os.getenv('MYSQL_PORT', '3306') + "/" + os.getenv('MYSQL_NAME', 'aktienbot') +SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning +SQLALCHEMY_ENGINE_OPTIONS = { + 'pool_size': 10, + 'pool_recycle': 60, + 'pool_pre_ping': True +} + +# openapi/Swagger config +BASE_RESPONSE_DATA_KEY = "data" +BASE_RESPONSE_SCHEMA = BaseResponseSchema + +BOT_EMAIL = os.getenv('BOT_EMAIL') +BOT_USERNAME = os.getenv('BOT_USERNAME') +BOT_PASSWORD = os.getenv('BOT_PASSWORD') + +ADMIN_EMAIL = os.getenv('ADMIN_EMAIL') +ADMIN_USERNAME = os.getenv('ADMIN_USERNAME') +ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD') diff --git a/api/app/config/flask_test.cfg b/api/app/config/flask_test.cfg new file mode 100644 index 0000000..fc8ac7f --- /dev/null +++ b/api/app/config/flask_test.cfg @@ -0,0 +1,32 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import os +from app.schema import BaseResponseSchema + +# Flask settings +SECRET_KEY = os.getenv('SECRET_KEY', 'secret string') + + +# Flask-SQLAlchemy settings +SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.getenv('MYSQL_USER') + ":" + os.getenv('MYSQL_PASSWORD') + "@" + os.getenv('MYSQL_HOST') + ":" + os.getenv('MYSQL_PORT', '3306') + "/" + os.getenv('MYSQL_NAME', 'aktienbot_test') +SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning +SQLALCHEMY_ENGINE_OPTIONS = { + 'pool_size': 100, + 'pool_recycle': 240 # 4 minutes +} + +# openapi/Swagger config +BASE_RESPONSE_DATA_KEY = "data" +BASE_RESPONSE_SCHEMA = BaseResponseSchema + +BOT_EMAIL = "bot1@example.com" +BOT_USERNAME = "bot1" +BOT_PASSWORD = "bot1" + +ADMIN_EMAIL = "admin1@example.com" +ADMIN_USERNAME = "admin1" +ADMIN_PASSWORD = "admin1" diff --git a/api/app/db.py b/api/app/db.py new file mode 100644 index 0000000..22b1d85 --- /dev/null +++ b/api/app/db.py @@ -0,0 +1,10 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +from flask_sqlalchemy import SQLAlchemy + +# database object +database = SQLAlchemy() diff --git a/api/app/helper_functions.py b/api/app/helper_functions.py new file mode 100644 index 0000000..560f114 --- /dev/null +++ b/api/app/helper_functions.py @@ -0,0 +1,136 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import bcrypt +import jwt +from apiflask import abort +from app.db import database as db +from app.models import User +from flask import current_app +from flask import request, jsonify + + +def hash_password(password): + """ + Hash plain password to save it in the database + """ + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) + + +def check_password(hashed_password, user_password): + """ + Check if the password is correct using the bcrypt checkpw function + """ + return bcrypt.checkpw(user_password, hashed_password) + + +def get_email_from_token_data(token): + """ + Extract email from token data + """ + + # If token is not provided-> return None + if token is None or len(token) < 2: + return None + else: + # Token contains "Bearer " -> remove it + token = token[1] + + # Again: Check if token is not None + # Don't know why, but sometimes the token is None + if token is not None: + + # We decided to append the user id to the bearer token using ":" as separator to select an specific user + # If the token contains ":" -> It may be a bot token + # If token valid -> return user email, not bot email + if ':' in token: + telegram_user_id = token.split(":")[1] + token = token.split(":")[0] + + try: + # Only allow selecting users with telegram_user_id if current user is the bot user + if jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])['email'] == current_app.config['BOT_EMAIL']: + res = db.session.query(User).filter_by(telegram_user_id=telegram_user_id).first() + + # Check if user id exists + if res is not None: + return res.as_dict()['email'] + else: + return None + else: + return None + except jwt.PyJWTError: + return None + + else: # "Normal" token, extract username from token + try: + # Return email from token if token is valid + return jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])['email'] + except jwt.PyJWTError: + return None + + +def get_token(): + """ + Extract token from Authorization header + """ + + # Check if Authorization header is provided + if 'Authorization' in request.headers: + return request.headers['Authorization'].split(" ") + else: + return None + + +def get_email_or_abort_401(): + """ + Try to receive email from token data + If email is not provided -> abort 401 + """ + email = get_email_from_token_data(get_token()) + + if email is None: # If token not provided or invalid -> return 401 code + abort(401, message="Unable to login") + + return email + + +def abort_if_no_admin(): + """ + Check if user is admin + If not -> abort 401 + """ + if not is_user_admin(): + abort(401, message="Only admin users can access this") + + +def is_user_admin(): + """ + Return users admin status + """ + email = get_email_or_abort_401() + + return db.session.query(User).filter_by(email=email).first().admin + + +def make_response(data, status=200, text=""): + """ + Generate response object + """ + return jsonify({"status": status, "text": text, "data": data}) + + +def get_user(email): + """ + Get user from database + """ + query_user = db.session.query(User).filter_by(email=email).first() + + # Check if user exists + if query_user is None: + abort(500, message="Can't find user") + + return query_user diff --git a/api/app/models.py b/api/app/models.py new file mode 100644 index 0000000..8846184 --- /dev/null +++ b/api/app/models.py @@ -0,0 +1,72 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.1" + +from app.db import database as db + + +class User(db.Model): + __tablename__ = 'users' + email = db.Column('email', db.String(255), primary_key=True, nullable=False, unique=True) + password = db.Column('password', db.BINARY(60), nullable=False) + username = db.Column('username', db.String(255), nullable=False, server_default='') + telegram_user_id = db.Column('telegram_user_id', db.String(255), nullable=True, server_default='') + admin = db.Column('admin', db.Boolean(), server_default='0') # 0 = False, 1 = True + cron = db.Column('cron', db.String(20), server_default='0 8 * * *', nullable=False) + + def as_dict(self): + return { + "email": self.email, + "username": self.username, + "telegram_user_id": self.telegram_user_id, + "admin": self.admin, + "cron": self.cron + } + + +class Transaction(db.Model): + __tablename__ = 'transactions' + t_id = db.Column('t_id', db.Integer(), nullable=False, unique=True, primary_key=True) + email = db.Column('email', db.String(255), db.ForeignKey('users.email', ondelete='CASCADE')) + isin = db.Column('isin', db.String(255)) + comment = db.Column('comment', 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) + email = db.Column('email', db.String(255), db.ForeignKey('users.email', 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) + email = db.Column('email', db.String(255), db.ForeignKey('users.email', ondelete='CASCADE')) + isin = db.Column('isin', db.String(255)) + comment = db.Column('comment', db.String(255)) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + +class SharePrice(db.Model): + __tablename__ = 'share_price' + id = db.Column('id', db.Integer(), nullable=False, unique=True, primary_key=True) + isin = db.Column('isin', db.String(255)) + price = db.Column('price', db.Float()) + date = db.Column('date', db.DateTime()) + + def as_dict(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} diff --git a/api/app/schema.py b/api/app/schema.py new file mode 100644 index 0000000..11be0e2 --- /dev/null +++ b/api/app/schema.py @@ -0,0 +1,137 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.1" + +from apiflask import Schema +from apiflask.fields import Integer, String, Boolean, Field, Float +from marshmallow import validate +from marshmallow.fields import Email + + +class BaseResponseSchema(Schema): + text = String() + status = Integer() + data = Field() + + +class UsersSchema(Schema): + admin = Boolean() + password = String() + username = String() + telegram_user_id = String() + email = Email() + cron = String() + + +class AdminDataSchema(Schema): + email = Email() + admin = Boolean() + + +class CronDataSchema(Schema): + cron = String() + + +class TokenSchema(Schema): + token = String() + + +class LoginDataSchema(Schema): + email = Email() + password = String() + + +class RegisterDataSchema(Schema): + email = Email() + username = String() + password = String() + + +class UpdateUserDataSchema(Schema): + username = String(required=False) + password = String(required=False) + + +class DeleteUserSchema(Schema): + email = Email() + + +class ChangePasswordSchema(Schema): + old_password = String() + new_password = String() + + +class ChangeUsernameSchema(Schema): + new_username = String() + + +class KeywordSchema(Schema): + keyword = String() + + +class SymbolSchema(Schema): + isin = String() + comment = String() + + +class SymbolRemoveSchema(Schema): + isin = String() + + +class TransactionSchema(Schema): + isin = String() + comment = String() + time = String(validate=validate.Regexp(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z")) + count = Integer() + price = Float() + + +class SymbolPriceSchema(Schema): + isin = String() + time = String(validate=validate.Regexp(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z")) + price = Float() + + +class TelegramIdSchema(Schema): + telegram_user_id = String() + + +class DeleteSuccessfulSchema(Schema): + pass + + +class KeywordResponseSchema(Schema): + keyword = String() + s_id = Integer() + email = Email() + + +class SymbolResponseSchema(Schema): + isin = String() + comment = String() + s_id = Integer() + email = Email() + + +class PortfolioShareResponseSchema(Schema): + count = Integer() + last_transaction = String() + + +class TransactionResponseSchema(Schema): + email = Email() + isin = String() + comment = String() + time = String() + count = Integer() + price = Float() + + +class PortfolioResponseSchema(Schema): + isin = String() + comment = String() + last_transaction = String() + count = Integer() + # price = Float() diff --git a/api/auth.py b/api/auth.py deleted file mode 100644 index 7db89a7..0000000 --- a/api/auth.py +++ /dev/null @@ -1,18 +0,0 @@ -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 diff --git a/api/config.py b/api/config.py deleted file mode 100644 index e7f323c..0000000 --- a/api/config.py +++ /dev/null @@ -1,52 +0,0 @@ -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 diff --git a/api/db.py b/api/db.py deleted file mode 100644 index f0b13d6..0000000 --- a/api/db.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask_sqlalchemy import SQLAlchemy - -db = SQLAlchemy() diff --git a/api/deploy/healthcheck.sh b/api/deploy/healthcheck.sh index 390f507..fffa122 100644 --- a/api/deploy/healthcheck.sh +++ b/api/deploy/healthcheck.sh @@ -1,2 +1,3 @@ #!/usr/bin/env sh + curl -s http://localhost:80/ -o /dev/null || exit 1 diff --git a/api/deploy/start.sh b/api/deploy/start.sh index 77fb780..db51705 100644 --- a/api/deploy/start.sh +++ b/api/deploy/start.sh @@ -1,3 +1,4 @@ #!/usr/bin/env sh + nginx -g "daemon off;" & uwsgi --ini deploy/uwsgi.ini \ No newline at end of file diff --git a/api/generate_sample_transactions.py b/api/generate_sample_transactions.py new file mode 100644 index 0000000..2c38856 --- /dev/null +++ b/api/generate_sample_transactions.py @@ -0,0 +1,31 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import os +import random + +import faker +import requests + +username = '' +password = '' + +shares = ["TWTR", "GOOG", "AAPL", "MSFT", "AMZN", "FB", "NFLX", "TSLA", "BABA", "BA", "BAC", "C", "CAT", "CSCO", "CVX", "DIS", "DOW", "DUK", "GE", "HD", "IBM" "INTC", "JNJ", "JPM", "KO", + "MCD", "MMM", "MRK", "NKE", "PFE", "PG", "T", "UNH", "UTX", "V", "VZ", "WMT", "XOM", "YHOO", "ZTS"] + +fake = faker.Faker() + +token = requests.post(os.getenv("API_URL") + '/user/login', json={"email": username, "password": password}).json()['data']['token'] + +for i in range(1, 10): + payload = { + "count": random.randint(1, 100), + "price": random.random() * 100, + "symbol": shares[random.randint(0, len(shares) - 1)], + "time": fake.date_time().isoformat() + ".000Z" + } + + response = requests.post(os.getenv("API_URL") + '/transaction', json=payload, headers={'Authorization': 'Bearer ' + token}) diff --git a/api/helper_functions.py b/api/helper_functions.py deleted file mode 100644 index 2791494..0000000 --- a/api/helper_functions.py +++ /dev/null @@ -1,73 +0,0 @@ -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 diff --git a/api/load_share_price.py b/api/load_share_price.py new file mode 100644 index 0000000..bd77ee4 --- /dev/null +++ b/api/load_share_price.py @@ -0,0 +1,50 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import datetime +import os +import threading +import time + +import requests +import yfinance +from dotenv import load_dotenv + + +def thread_function(s): + my_share_info = yfinance.Ticker(s) + my_share_data = my_share_info.info + + if my_share_data['regularMarketPrice'] is not None: + payload = { + "symbol": s, + "price": float(my_share_data['regularMarketPrice']), + "time": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000Z") + } + + requests.post(os.getenv("API_URL") + '/symbol', json=payload, headers={'Authorization': 'Bearer ' + token}) + + +def split(a, n): + k, m = divmod(len(a), n) + return (a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)) + + +load_dotenv() +username = os.getenv('ADMIN_EMAIL') +password = os.getenv('ADMIN_PASSWORD') + +token = requests.post(os.getenv("API_URL") + '/user/login', json={"email": username, "password": password}).json()['data']['token'] + +response = requests.get(os.getenv("API_URL") + '/symbols', headers={'Authorization': 'Bearer ' + token}).json()['data'] + +symbols = split(response, int(len(response) / 5)) +for symbol_list in symbols: + for symbol in symbol_list: + x = threading.Thread(target=thread_function, args=(symbol,)) + x.start() + + time.sleep(10) diff --git a/api/models.py b/api/models.py deleted file mode 100644 index 59e4287..0000000 --- a/api/models.py +++ /dev/null @@ -1,46 +0,0 @@ -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} diff --git a/api/requirements.txt b/api/requirements.txt index d37bc8e..90bed9a 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,12 +1,16 @@ -Flask~=2.0.3 -python-dotenv==0.19.2 -requests==2.27.1 +Flask~=2.1.1 +python-dotenv==0.20.0 uwsgi==2.0.20 Flask_SQLAlchemy==2.5.1 -python-dotenv==0.19.2 +python-dotenv==0.20.0 pymysql==1.0.2 -pyjwt==2.0.0 +pyjwt==2.3.0 apiflask==0.12.0 -flask-swagger-ui==3.36.0 flask-cors==3.0.10 - +bcrypt==3.2.0 +pytest~=7.1.2 +pytest-cov +marshmallow~=3.15.0 +faker~=13.6.0 +yfinance~=0.1.70 +requests~=2.27.1 \ No newline at end of file diff --git a/api/scheme.py b/api/scheme.py deleted file mode 100644 index bb1371a..0000000 --- a/api/scheme.py +++ /dev/null @@ -1,80 +0,0 @@ -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 diff --git a/api/tests/conftest.py b/api/tests/conftest.py new file mode 100644 index 0000000..be4933c --- /dev/null +++ b/api/tests/conftest.py @@ -0,0 +1,111 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import pytest +from app import create_app, db +from app.models import User, Transaction, Keyword, Share + +from app.helper_functions import hash_password + + +@pytest.fixture(scope='module') +def new_user(): + user = User( + email="user@example.com", + username="user", + password=hash_password("password"), + admin=False + ) + return user + + +@pytest.fixture(scope='module') +def new_transaction(): + transaction = Transaction( + email="user@example.com", + symbol="DTEGY", + time="2022-03-29T10:00:00.000Z", + count=10, + price=9.99 + ) + return transaction + + +@pytest.fixture(scope='module') +def new_keyword(): + keyword = Keyword( + email="user@example.com", + keyword="Elon Musk", + ) + return keyword + + +@pytest.fixture(scope='module') +def new_share(): + share = Share( + email="user@example.com", + symbol="DTEGY", + ) + return share + + +@pytest.fixture(scope='module') +def test_client(): + flask_app = create_app('config/flask_test.cfg') + + # Create a test client using the Flask application configured for testing + with flask_app.test_client() as testing_client: + # Establish an application context + with flask_app.app_context(): + yield testing_client # this is where the testing happens! + + +@pytest.fixture(scope='function') +def init_database(test_client): + # Create the database and the database table + db.create_all() + + # Insert user data + user1 = User( + email="user1@example.com", + username="user1", + password=hash_password("password"), + telegram_user_id="12345678", + admin=False + ) + user2 = User( + email="user2@example.com", + username="user2", + password=hash_password("password"), + telegram_user_id="87654321", + admin=False + ) + admin = User( + email="admin1@example.com", + username="admin1", + password=hash_password("admin1"), + telegram_user_id="00000000", + admin=True + ) + bot = User( + email="bot1@example.com", + username="bot1", + password=hash_password("bot1"), + telegram_user_id="00000000", + admin=False + ) + db.session.add(user1) + db.session.add(user2) + db.session.add(admin) + db.session.add(bot) + + # Commit the changes for the users + db.session.commit() + + yield # this is where the testing happens! + + db.session.commit() + db.drop_all() diff --git a/api/tests/functional/__init__.py b/api/tests/functional/__init__.py new file mode 100644 index 0000000..dfaac6b --- /dev/null +++ b/api/tests/functional/__init__.py @@ -0,0 +1,5 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" diff --git a/api/tests/functional/helper_functions.py b/api/tests/functional/helper_functions.py new file mode 100644 index 0000000..8c93f71 --- /dev/null +++ b/api/tests/functional/helper_functions.py @@ -0,0 +1,17 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +import json + + +def get_token(test_client, email, password): + response = test_client.post('/api/user/login', data=json.dumps(dict(email=email, password=password)), content_type='application/json') + + if "data" in json.loads(response.data): + if "token" in json.loads(response.data)["data"]: + return json.loads(response.data)["data"]["token"] + + return "" diff --git a/api/tests/functional/test_keyword.py b/api/tests/functional/test_keyword.py new file mode 100644 index 0000000..3ce610d --- /dev/null +++ b/api/tests/functional/test_keyword.py @@ -0,0 +1,188 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_keyword.py) contains the functional tests for the `keyword` blueprint. +""" +import json +from tests.functional.helper_functions import get_token + + +def test_add_keyword_not_logged_in(test_client, init_database): + """ + Test POST '/api/keyword' + + User is not logged in + """ + response = test_client.post('/api/keyword') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_add_keyword_user1_logged_in(test_client, init_database): + """ + Test POST '/api/keyword' + + User1 is logged in + """ + response = test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully added keyword' in response.data + + +def test_add_keyword_user1_logged_in_but_keyword_exist(test_client, init_database): + """ + Test POST '/api/keyword' + + User1 is logged in + Add keyword two times + """ + test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + response = test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 500 + assert b'Keyword already exist for this user' in response.data + + +def test_add_keyword_user1_logged_in_but_keyword_missing(test_client, init_database): + """ + Test POST '/api/keyword' + + User1 is logged in + Keyword is missing in post data + """ + response = test_client.post('/api/keyword', data=json.dumps(dict()), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_add_keyword_user1_logged_in_but_keyword_empty(test_client, init_database): + """ + Test POST '/api/keyword' + + User1 is logged in + Keyword is empty in post data + """ + response = test_client.post('/api/keyword', data=json.dumps(dict(keyword="")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_get_keyword_not_logged_in(test_client, init_database): + """ + Test GET '/api/keyword' + + User is not logged in + """ + response = test_client.get('/api/keywords') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_get_keyword_user1_logged_in_empty_response(test_client, init_database): + """ + Test GET '/api/keyword' + + User1 is logged in + Empty response + """ + response = test_client.get('/api/keywords', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 200 + assert b'"data":[' in response.data + + +def test_get_keyword_user1_logged_in_response_data(test_client, init_database): + """ + Test GET '/api/keyword' + + User1 is logged in + Create some keywords for user1 + """ + test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + response = test_client.get('/api/keywords', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 200 + assert b'"data":[' in response.data + + +def test_delete_keyword_not_logged_in(test_client, init_database): + """ + Test DELETE '/api/keyword' + + User is not logged in + """ + response = test_client.delete('/api/keyword') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_delete_keyword_user1_logged_in_but_empty_keyword(test_client, init_database): + """ + Test DELETE '/api/keyword' + + User1 is logged in + Keyword empty in in delete data + """ + response = test_client.delete('/api/keyword', data=json.dumps(dict(keyword="")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_delete_keyword_user1_logged_in_but_missing_keyword(test_client, init_database): + """ + Test DELETE '/api/keyword' + + User1 is logged in + Keyword missing in in delete data + """ + response = test_client.delete('/api/keyword', data=json.dumps(dict()), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_delete_keyword_user1_logged_in_keyword_exists(test_client, init_database): + """ + Test DELETE '/api/keyword' + + User1 is logged in + Keyword exists + """ + test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + response = test_client.delete('/api/keyword', data=json.dumps(dict(keyword="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully removed keyword' in response.data + + +def test_delete_keyword_user1_logged_in_keyword_not_exists(test_client, init_database): + """ + Test DELETE '/api/keyword' + + User1 is logged in + Keyword doesn't exists + """ + response = test_client.delete('/api/keyword', data=json.dumps(dict(keyword="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 500 + assert b'Keyword doesn\'t exist for this user' in response.data diff --git a/api/tests/functional/test_portfolio.py b/api/tests/functional/test_portfolio.py new file mode 100644 index 0000000..b2278ba --- /dev/null +++ b/api/tests/functional/test_portfolio.py @@ -0,0 +1,56 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_portfolio.py) contains the functional tests for the `portfolio` blueprint. +""" +import json +from tests.functional.helper_functions import get_token + + +def test_get_portfolio_not_logged_in_empty_response(test_client, init_database): + """ + Test GET '/api/portfolio' + + User is not logged in + """ + response = test_client.get('/api/portfolio') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_get_portfolio_user1_logged_in_empty_response(test_client, init_database): + """ + Test GET '/api/portfolio' + + User1 is logged in + Empty response + """ + response = test_client.get('/api/portfolio', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'"data":[]' in response.data + assert b'Successfully loaded symbols' in response.data + + +def test_get_portfolio_user1_logged_in_response_data(test_client, init_database): + """ + Test GET '/api/portfolio' + + User1 is logged in + Create transaction data + """ + test_client.post('/api/transaction', data=json.dumps(dict(count=5, price=9.99, symbol="DTEGY", time="2022-03-29T10:00:00.000Z")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + + response = test_client.get('/api/portfolio', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'"data":[]' not in response.data + assert b'"data":[' in response.data diff --git a/api/tests/functional/test_share.py b/api/tests/functional/test_share.py new file mode 100644 index 0000000..875bc0b --- /dev/null +++ b/api/tests/functional/test_share.py @@ -0,0 +1,188 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_share.py) contains the functional tests for the `share` blueprint. +""" +import json +from tests.functional.helper_functions import get_token + + +def test_add_share_not_logged_in(test_client, init_database): + """ + Test POST '/api/share' + + User is not logged in + """ + response = test_client.post('/api/share') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_add_share_user1_logged_in(test_client, init_database): + """ + Test POST '/api/share' + + User1 is logged in + """ + response = test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully added symbol' in response.data + + +def test_add_share_user1_logged_in_but_symbol_exist(test_client, init_database): + """ + Test POST '/api/share' + + User1 is logged in + Add symbol two times + """ + test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + response = test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 500 + assert b'Symbol already exist for this user' in response.data + + +def test_add_share_user1_logged_in_but_symbol_missing(test_client, init_database): + """ + Test POST '/api/share' + + User1 is logged in + Symbol is missing in post data + """ + response = test_client.post('/api/share', data=json.dumps(dict()), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_add_share_user1_logged_in_but_symbol_empty(test_client, init_database): + """ + Test POST '/api/share' + + User1 is logged in + Symbol is empty in post data + """ + response = test_client.post('/api/share', data=json.dumps(dict(symbol="")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_get_share_not_logged_in(test_client, init_database): + """ + Test GET '/api/share' + + User is not logged in + """ + response = test_client.get('/api/shares') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_get_share_user1_logged_in_empty_response(test_client, init_database): + """ + Test GET '/api/share' + + User1 is logged in + Empty response + """ + response = test_client.get('/api/shares', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 200 + assert b'"data":[' in response.data + + +def test_get_share_user1_logged_in_response_data(test_client, init_database): + """ + Test GET '/api/share' + + User1 is logged in + Create some symbols for user1 + """ + test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + response = test_client.get('/api/shares', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 200 + assert b'"data":[' in response.data + + +def test_delete_share_not_logged_in(test_client, init_database): + """ + Test DELETE '/api/share' + + User is not logged in + """ + response = test_client.delete('/api/share') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_delete_share_user1_logged_in_but_empty_symbol(test_client, init_database): + """ + Test DELETE '/api/share' + + User1 is logged in + Symbol empty in in delete data + """ + response = test_client.delete('/api/share', data=json.dumps(dict(symbol="")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_delete_share_user1_logged_in_but_missing_symbol(test_client, init_database): + """ + Test DELETE '/api/share' + + User1 is logged in + Symbol missing in in delete data + """ + response = test_client.delete('/api/share', data=json.dumps(dict()), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_delete_share_user1_logged_in_symbol_exists(test_client, init_database): + """ + Test DELETE '/api/share' + + User1 is logged in + Symbol exists + """ + test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + response = test_client.delete('/api/share', data=json.dumps(dict(symbol="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully removed symbol' in response.data + + +def test_delete_share_user1_logged_in_symbol_not_exists(test_client, init_database): + """ + Test DELETE '/api/share' + + User1 is logged in + Symbol doesn't exists + """ + response = test_client.delete('/api/share', data=json.dumps(dict(symbol="DTEGY")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 500 + assert b'Symbol doesn\'t exist for this user' in response.data diff --git a/api/tests/functional/test_telegram.py b/api/tests/functional/test_telegram.py new file mode 100644 index 0000000..17679df --- /dev/null +++ b/api/tests/functional/test_telegram.py @@ -0,0 +1,63 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_telegram.py) contains the functional tests for the `telegram` blueprint. +""" +import json +from tests.functional.helper_functions import get_token + + +def test_add_telegram_not_logged_in(test_client, init_database): + """ + Test POST '/api/telegram' + + User is not logged in + """ + response = test_client.post('/api/telegram') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_add_telegram_user1_logged_in(test_client, init_database): + """ + Test POST '/api/telegram' + + User1 is logged in + """ + response = test_client.post('/api/telegram', data=json.dumps(dict(telegram_user_id="12345678")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully connected telegram user' in response.data + + +def test_add_telegram_user1_logged_in_user_data_missing(test_client, init_database): + """ + Test POST '/api/telegram' + + User1 is logged in + telegram_user_id is missing + """ + response = test_client.post('/api/telegram', data=json.dumps(dict()), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_add_telegram_user1_logged_in_user_data_empty(test_client, init_database): + """ + Test POST '/api/telegram' + + User1 is logged in + telegram_user_id is empty + """ + response = test_client.post('/api/telegram', data=json.dumps(dict(telegram_user_id="")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data diff --git a/api/tests/functional/test_transaction.py b/api/tests/functional/test_transaction.py new file mode 100644 index 0000000..2f1917d --- /dev/null +++ b/api/tests/functional/test_transaction.py @@ -0,0 +1,100 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_transaction.py) contains the functional tests for the `transaction` blueprint. +""" +import json +from tests.functional.helper_functions import get_token + + +def test_add_transaction_not_logged_in(test_client, init_database): + """ + Test POST '/api/transaction' + + User is not logged in + """ + response = test_client.get('/api/portfolio') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_add_transaction_user1_logged_in_missing_data(test_client, init_database): + """ + Test POST '/api/transaction' + + User1 is logged in + Data missing + """ + # symbol missing + response = test_client.post('/api/transaction', data=json.dumps(dict(time="2022-03-29T10:00:00.000Z", count=10, price=9.99)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + # time missing + response = test_client.post('/api/transaction', data=json.dumps(dict(symbol="DTEGY", count=10, price=9.99)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + # count missing + response = test_client.post('/api/transaction', data=json.dumps(dict(symbol="DTEGY", time="2022-03-29T10:00:00.000Z", price=9.99)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + # price missing + response = test_client.post('/api/transaction', data=json.dumps(dict(symbol="DTEGY", time="2022-03-29T10:00:00.000Z", count=10)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_get_transaction_not_logged_in(test_client, init_database): + """ + Test GET '/api/transaction' + + User is not logged in + """ + response = test_client.get('/api/transactions') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_get_transaction_user1_logged_in_empty_response(test_client, init_database): + """ + Test GET '/api/transaction' + + User1 is logged in + """ + response = test_client.get('/api/transactions', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully loaded transactions' in response.data + + +def test_get_transaction_user1_logged_in_response_data(test_client, init_database): + """ + Test GET '/api/transaction' + + User1 is logged in + Create transaction + """ + test_client.post('/api/transaction', data=json.dumps(dict(count=5, price=9.99, symbol="DTEGY", time="2022-03-29T10:00:00.000Z")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + + response = test_client.get('/api/transactions', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully loaded transactions' in response.data diff --git a/api/tests/functional/test_user.py b/api/tests/functional/test_user.py new file mode 100644 index 0000000..8b4c1eb --- /dev/null +++ b/api/tests/functional/test_user.py @@ -0,0 +1,599 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_user.py) contains the functional tests for the `users` blueprint. +""" +import json + +from tests.functional.helper_functions import get_token + + +def test_login_with_valid_data(test_client, init_database): + """ + Test POST '/api/user/login' + + Valid data + """ + response = test_client.post('/api/user/login', data=json.dumps(dict(email="user1@example.com", password="password")), content_type='application/json') + assert response.status_code == 200 + assert b'Successfully logged in' in response.data + + +def test_login_with_wrong_password(test_client, init_database): + """ + Test POST '/api/user/login' + + Wrong password + """ + response = test_client.post('/api/user/login', data=json.dumps(dict(email="user2@example.com", password="password2")), content_type='application/json') + assert response.status_code == 500 + assert b'Unable to login' in response.data + + +def test_login_user_not_exist(test_client, init_database): + """ + Test POST '/api/user/login' + + User doesn't exist + """ + response = test_client.post('/api/user/login', data=json.dumps(dict(email="notexistinguser@example.com", password="password")), content_type='application/json') + assert response.status_code == 500 + assert b'Can\'t find user' in response.data + + +def test_login_email_missing(test_client, init_database): + """ + Test POST '/api/user/login' + + Email missing + """ + response = test_client.post('/api/user/login', data=json.dumps(dict(password="password")), content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_login_password_missing(test_client, init_database): + """ + Test POST '/api/user/login' + + Password missing + """ + response = test_client.post('/api/user/login', data=json.dumps(dict(email="user1@example.com")), content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_register_valid_data(test_client, init_database): + """ + Test POST '/api/user/register' + + Valid data + """ + response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + assert response.status_code == 200 + assert b'Successfully registered user' in response.data + + +def test_register_user_exists_already(test_client, init_database): + """ + Test POST '/api/user/register' + + User exists already + """ + test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + assert response.status_code == 500 + assert b'Email already exist' in response.data + + +def test_register_email_missing(test_client, init_database): + """ + Test POST '/api/user/register' + + Email missing + """ + response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", username="user3")), content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_register_email_empty(test_client, init_database): + """ + Test POST '/api/user/register' + + Email empty + """ + response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", username="user3", email="")), content_type='application/json') + assert response.status_code == 400 + assert b'Not a valid email address' in response.data + + +def test_register_password_missing(test_client, init_database): + """ + Test POST '/api/user/register' + + Password missing + """ + response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", username="user3")), content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_register_password_empty(test_client, init_database): + """ + Test POST '/api/user/register' + + password empty + """ + response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", username="user3", password="")), content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_register_username_missing(test_client, init_database): + """ + Test POST '/api/user/register' + + Username missing + """ + response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", email="user3@example.com")), content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_register_username_empty(test_client, init_database): + """ + Test POST '/api/user/register' + + Username empty + """ + response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", username="", email="user3@example.com")), content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_delete_user_not_logged_in(test_client, init_database): + """ + Test DELETE '/api/user' + + User is not logged in + """ + test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + response = test_client.delete('/api/user', data=json.dumps(dict(email="user3@example.com")), content_type='application/json') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_delete_user_same_user_logged_in(test_client, init_database): + """ + Test DELETE '/api/user' + + User3 is logged in + """ + test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + response = test_client.delete('/api/user', + data=json.dumps(dict(email="user3@example.com")), + content_type='application/json', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user3@example.com", "password"))}) + assert response.status_code == 200 + assert b'Successfully removed user' in response.data + + +def test_delete_user_different_user_logged_in_no_admin(test_client, init_database): + """ + Test DELETE '/api/user' + + Different user is logged in -> no admin + """ + test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + response = test_client.delete('/api/user', + data=json.dumps(dict(email="user3@example.com")), + content_type='application/json', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 401 + assert b'Only admin users can access this' in response.data + + +def test_delete_user_different_user_logged_in_admin(test_client, init_database): + """ + Test DELETE '/api/user' + + Different user is logged in -> admin + """ + test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + response = test_client.delete('/api/user', + data=json.dumps(dict(email="user3@example.com")), + content_type='application/json', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}) + assert response.status_code == 200 + assert b'Successfully removed user' in response.data + + +def test_delete_user_email_missing(test_client, init_database): + """ + Test DELETE '/api/user' + + Email missing + """ + test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + response = test_client.delete('/api/user', + data=json.dumps(dict()), + content_type='application/json', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user3@example.com", "password"))}) + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_delete_user_email_empty(test_client, init_database): + """ + Test DELETE '/api/user' + + Email empty + """ + test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json') + response = test_client.delete('/api/user', + data=json.dumps(dict(email="")), + content_type='application/json', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user3@example.com", "password"))}) + assert response.status_code == 400 + assert b'Not a valid email address' in response.data + + +def test_get_current_user_user_not_logged_in(test_client, init_database): + """ + Test GET '/api/user' + + User is not logged in + """ + response = test_client.get('/api/user') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_get_current_user_user1_logged_in(test_client, init_database): + """ + Test GET '/api/user' + + User1 is logged in + """ + response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 200 + assert b'user1' in response.data + assert b'user2' not in response.data + + +def test_get_current_user_bot_logged_in_user_exists(test_client, init_database): + """ + Test GET '/api/user' + + Bot1 is logged in and requests user 12345678 + """ + response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":12345678")}) + assert response.status_code == 200 + assert b'user1' in response.data + assert b'bot' not in response.data + + +def test_get_current_user_bot_logged_in_user_not_exists(test_client, init_database): + """ + Test GET '/api/user' + + Bot1 is logged in and requests user 1234 (not existing) + """ + response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":1234")}) + assert response.status_code == 401 + assert b'Unable to login' in response.data + + +def test_get_current_user_user1_logged_in_but_no_bot(test_client, init_database): + """ + Test GET '/api/user' + + User1 is logged in and requests user 1234 (not existing) + Fails because user1 is not a bot + """ + response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password") + ":12345678")}) + assert response.status_code == 401 + assert b'Unable to login' in response.data + + +def test_get_current_user_invalid_token(test_client, init_database): + """ + Test GET '/api/user' + + Invalid Bearer token + """ + response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format("invalidtoken:12345678")}) + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_update_user_not_logged_in(test_client, init_database): + """ + Test PUT '/api/user' + + User is not logged in + """ + response = test_client.put('/api/user') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_update_user1_logged_in_password_username(test_client, init_database): + """ + Test PUT '/api/user' + + User1 is logged in + Change Username and Password + """ + test_client.put('/api/user', data=json.dumps(dict(username="user4", password="password")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + response = test_client.get('/api/user', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'user4' in response.data + + +def test_update_user1_logged_in_password(test_client, init_database): + """ + Test PUT '/api/user' + + User1 is logged in + Change Password + """ + response = test_client.put('/api/user', data=json.dumps(dict(password="password123")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 200 + assert b'Successfully updated user' in response.data + + +def test_update_user1_logged_in_username(test_client, init_database): + """ + Test PUT '/api/user' + + User1 is logged in + Change Username + """ + response = test_client.put('/api/user', data=json.dumps(dict(username="user1")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}) + assert response.status_code == 200 + assert b'Successfully updated user' in response.data + + +def test_set_admin_user_not_logged_in(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + User is not logged in + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=True)), content_type='application/json') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_set_admin_user1_logged_in(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + User1 is logged in (no admin) + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=True)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 401 + assert b'Only admin users can access this' in response.data + + +def test_set_admin_admin1_logged_in(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + Admin1 is logged in (admin) + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=True)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully updated users admin rights' in response.data + + +def test_set_admin_admin1_logged_in_user_not_exist(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + Admin1 is logged in (admin) + notexistinguser@example.com does not exist + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="notexistinguser@example.com", admin=True)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, + content_type='application/json') + assert response.status_code == 500 + assert b'Can\'t find user' in response.data + + +def test_set_admin_admin1_logged_in_email_missing(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + Admin1 is logged in (admin) + email missing + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(admin=True)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_set_admin_admin1_logged_in_email_empty(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + Admin1 is logged in (admin) + email missing + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="", admin=True)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'Not a valid email address.' in response.data + + +def test_set_admin_admin1_logged_in_admin_missing(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + Admin1 is logged in (admin) + admin data missing + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'missing' in response.data + + +def test_set_admin_admin1_logged_in_admin_empty(test_client, init_database): + """ + Test PUT '/api/user/setAdmin' + + Admin1 is logged in (admin) + admin data missing + """ + response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=None)), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, + content_type='application/json') + assert response.status_code == 400 + assert b'Field may not be null' in response.data + + +def test_set_cron_user_not_logged_in(test_client, init_database): + """ + Test PUT '/api/user/setCron' + + User is not logged in + """ + response = test_client.put('/api/user/setCron') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_set_cron_user1_logged_in(test_client, init_database): + """ + Test PUT '/api/user/setCron' + + User1 is logged in + """ + response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully updated users cron' in response.data + + +def test_set_empty_cron_user1_logged_in(test_client, init_database): + """ + Test PUT '/api/user/setCron' + + User1 is logged in + Interval is empty + """ + response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + assert response.status_code == 400 + + +def test_set_cron_bot_logged_in_user_exists(test_client, init_database): + """ + Test PUT '/api/user/setCron' + + Bot1 is logged in and requests user 12345678 + """ + response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":12345678")}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully updated users cron' in response.data + + +def test_set_cron_bot_logged_in_user_not_exists(test_client, init_database): + """ + Test PUT '/api/user/setCron' + + Bot1 is logged in and requests user 1234 (not existing) + """ + response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":1234")}, + content_type='application/json') + assert response.status_code == 401 + assert b'Unable to login' in response.data + + +def test_set_cron_user1_logged_in_but_no_bot(test_client, init_database): + """ + Test PUT '/api/user/setCron' + + User1 is logged in and requests user 1234 (not existing) + Fails because user1 is not a bot + """ + response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")), + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password") + ":12345678")}, + content_type='application/json') + assert response.status_code == 401 + assert b'Unable to login' in response.data + + +def test_set_cron_invalid_token(test_client, init_database): + """ + Test PUT '/api/user/setCron' + + Invalid Bearer token + """ + response = test_client.put('/api/user/setCron', headers={"Authorization": "Bearer {}".format("invalidtoken:12345678")}) + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_get_users_not_logged_in(test_client, init_database): + """ + Test GET '/api/users' + + User is not logged in + """ + response = test_client.get('/api/users') + assert response.status_code == 401 + assert b'Unauthorized' in response.data + + +def test_get_users_user1_logged_in(test_client, init_database): + """ + Test GET '/api/users' + + User1 is logged in (not admin) + """ + response = test_client.get('/api/users', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))}, + content_type='application/json') + + assert response.status_code == 401 + assert b'Only admin users can access this' in response.data + + +def test_get_users_admin1_logged_in(test_client, init_database): + """ + Test GET '/api/users' + + Admin1 is logged in (admin) + """ + response = test_client.get('/api/users', + headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, + content_type='application/json') + assert response.status_code == 200 + assert b'Successfully received all users' in response.data diff --git a/api/tests/pytest.ini b/api/tests/pytest.ini new file mode 100644 index 0000000..e69de29 diff --git a/api/tests/unit/__init__.py b/api/tests/unit/__init__.py new file mode 100644 index 0000000..dfaac6b --- /dev/null +++ b/api/tests/unit/__init__.py @@ -0,0 +1,5 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" diff --git a/api/tests/unit/test_auth.py b/api/tests/unit/test_auth.py new file mode 100644 index 0000000..110e7ae --- /dev/null +++ b/api/tests/unit/test_auth.py @@ -0,0 +1,17 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_helper_functions.py) contains the unit tests for the helper_functions.py file. +""" +from app.auth import * + + +def test_verify_token(): + """ + Test verify_token function + """ + assert verify_token(None) is False diff --git a/api/tests/unit/test_helper_functions.py b/api/tests/unit/test_helper_functions.py new file mode 100644 index 0000000..0c61bcf --- /dev/null +++ b/api/tests/unit/test_helper_functions.py @@ -0,0 +1,33 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_helper_functions.py) contains the unit tests for the helper_functions.py file. +""" +from app.helper_functions import * + + +def test_hash_password(): + """ + Test hash_password function + """ + assert hash_password("password") != "password" + + +def test_check_password(): + """ + Test check_password function + """ + hashed = hash_password("password") + assert check_password(hashed, "password".encode("utf-8")) is True + assert check_password(hashed, "password1".encode("utf-8")) is False + + +def test_get_email_from_token(): + """ + Test get_email_from_token function + """ + assert get_email_from_token_data(None) is None diff --git a/api/tests/unit/test_models.py b/api/tests/unit/test_models.py new file mode 100644 index 0000000..57618a9 --- /dev/null +++ b/api/tests/unit/test_models.py @@ -0,0 +1,122 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_models.py) contains the unit tests for the models.py file. +""" +from app.helper_functions import hash_password +from app.models import User, Transaction, Keyword, Share + + +def test_new_user(): + """ + GIVEN a User model + WHEN a new User is created + THEN check the email, password, username, telegram_user_id and admin fields are defined correctly + """ + user = User( + email="user@example.com", + username="user", + password=hash_password("password"), + admin=False, + cron="0 8 * * *", + ) + assert user.email == 'user@example.com' + assert user.password != 'password' + assert user.username == "user" + assert user.telegram_user_id is None + assert user.admin is False + assert user.as_dict() == {'cron': '0 8 * * *', 'email': 'user@example.com', 'username': 'user', 'telegram_user_id': None, 'admin': False} + + +def test_new_user_with_fixture(new_user): + """ + GIVEN a User model + WHEN a new User is created + THEN check the email and password_hashed fields are defined correctly + """ + assert new_user.email == 'user@example.com' + assert new_user.password != 'password' + + +def test_new_transaction(): + """ + GIVEN a Transaction model + WHEN a new Transaction is created + THEN check the email, symbol, time count and price fields are defined correctly + """ + transaction = Transaction( + email="user@example.com", + symbol="DTEGY", + time="2022-03-29T10:00:00.000Z", + count=10, + price=9.99 + ) + assert transaction.email == 'user@example.com' + assert transaction.symbol == "DTEGY" + assert transaction.count == 10 + assert transaction.price == 9.99 + assert transaction.as_dict() == {'t_id': None, 'email': 'user@example.com', 'symbol': 'DTEGY', 'time': '2022-03-29T10:00:00.000Z', 'count': 10, 'price': 9.99} + + +def test_new_transaction_with_fixture(new_transaction): + """ + GIVEN a User model + WHEN a new User is created + THEN check the email and password_hashed fields are defined correctly + """ + assert new_transaction.email == 'user@example.com' + assert new_transaction.symbol == 'DTEGY' + + +def test_new_keyword(): + """ + GIVEN a Transaction model + WHEN a new Transaction is created + THEN check the email, symbol, time count and price fields are defined correctly + """ + keyword = Keyword( + email="user@example.com", + keyword="Elon Musk" + ) + assert keyword.email == 'user@example.com' + assert keyword.keyword == "Elon Musk" + assert keyword.as_dict() == {'s_id': None, 'email': 'user@example.com', 'keyword': 'Elon Musk'} + + +def test_new_keyword_with_fixture(new_keyword): + """ + GIVEN a User model + WHEN a new User is created + THEN check the email and password_hashed fields are defined correctly + """ + assert new_keyword.email == 'user@example.com' + assert new_keyword.keyword == 'Elon Musk' + + +def test_new_share(): + """ + GIVEN a Transaction model + WHEN a new Transaction is created + THEN check the email, symbol, time count and price fields are defined correctly + """ + share = Share( + email="user@example.com", + symbol="DTEGY" + ) + assert share.email == 'user@example.com' + assert share.symbol == "DTEGY" + assert share.as_dict() == {'a_id': None, 'email': 'user@example.com', 'symbol': 'DTEGY'} + + +def test_new_share_with_fixture(new_share): + """ + GIVEN a User model + WHEN a new User is created + THEN check the email and password_hashed fields are defined correctly + """ + assert new_share.email == 'user@example.com' + assert new_share.symbol == 'DTEGY' diff --git a/api/tests/unit/test_transaction.py b/api/tests/unit/test_transaction.py new file mode 100644 index 0000000..91f8e25 --- /dev/null +++ b/api/tests/unit/test_transaction.py @@ -0,0 +1,46 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_transaction.py) contains the unit tests for the blueprints/transaction.py file. +""" +from app.blueprints.transactions import * + + +def test_check_if_symbol_data_exists(): + """ + Test check_if_symbol_data_exists function + """ + assert check_if_symbol_data_exists(dict(symbol='DTEGY')) is True + assert check_if_symbol_data_exists(dict(symbol='')) is False + assert check_if_symbol_data_exists(dict()) is False + + +def test_check_if_time_data_exists(): + """ + Test check_if_time_data_exists function + """ + assert check_if_time_data_exists(dict(time='2022-03-29T10:00:00.000Z')) is True + assert check_if_time_data_exists(dict(time='')) is False + assert check_if_time_data_exists(dict()) is False + + +def test_check_if_count_data_exists(): + """ + Test check_if_count_data_exists function + """ + assert check_if_count_data_exists(dict(count=10)) is True + assert check_if_count_data_exists(dict(count=None)) is False + assert check_if_count_data_exists(dict()) is False + + +def test_check_if_price_data_exists(): + """ + Test check_if_price_data_exists function + """ + assert check_if_price_data_exists(dict(price=9.99)) is True + assert check_if_price_data_exists(dict(price=None)) is False + assert check_if_price_data_exists(dict()) is False diff --git a/api/tests/unit/test_user.py b/api/tests/unit/test_user.py new file mode 100644 index 0000000..2846fe7 --- /dev/null +++ b/api/tests/unit/test_user.py @@ -0,0 +1,55 @@ +__author__ = "Florian Kaiser" +__copyright__ = "Copyright 2022, Project Aktienbot" +__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"] +__license__ = "GPL 3.0" +__version__ = "1.0.0" + +""" +This file (test_helper_functions.py) contains the unit tests for the helper_functions.py file. +""" +from app.blueprints.user import * + + +def test_check_if_email_data_exists(): + """ + Test check_if_email_data_exists function + """ + assert check_if_email_data_exists(dict(email='user@example.com')) is True + assert check_if_email_data_exists(dict(email='')) is False + assert check_if_email_data_exists(dict()) is False + + +def test_check_if_password_data_exists(): + """ + Test check_if_password_data_exists function + """ + assert check_if_password_data_exists(dict(password='password')) is True + assert check_if_password_data_exists(dict(password='')) is False + assert check_if_password_data_exists(dict()) is False + + +def test_check_if_username_data_exists(): + """ + Test check_if_username_data_exists function + """ + assert check_if_username_data_exists(dict(username='user')) is True + assert check_if_username_data_exists(dict(username='')) is False + assert check_if_username_data_exists(dict()) is False + + +def test_check_if_admin_data_exists(): + """ + Test check_if_admin_data_exists function + """ + assert check_if_admin_data_exists(dict(admin=True)) is True + assert check_if_admin_data_exists(dict(admin=None)) is False + assert check_if_admin_data_exists(dict()) is False + + +def test_check_if_cron_data_exists(): + """ + Test check_if_cron_data_exists function + """ + assert check_if_cron_data_exists(dict(cron="* * * * *")) is True + assert check_if_cron_data_exists(dict(cron="")) is False + assert check_if_cron_data_exists(dict()) is False diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..e7cf9f2 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,3 @@ +# Deploy + +Files that are used for deployment. diff --git a/deploy/aktienbot/.env.api b/deploy/aktienbot/.env.api new file mode 100644 index 0000000..81a2bf6 --- /dev/null +++ b/deploy/aktienbot/.env.api @@ -0,0 +1,16 @@ +BOT_API_KEY= +SECRET_KEY= + +MYSQL_USER= +MYSQL_PASSWORD= +MYSQL_HOST= +MYSQL_PORT= +MYSQL_DATABASE= + +BOT_EMAIL= +BOT_USERNAME= +BOT_PASSWORD= + +ADMIN_EMAIL= +ADMIN_USERNAME= +ADMIN_PASSWORD= diff --git a/deploy/aktienbot/.env.bot b/deploy/aktienbot/.env.bot new file mode 100644 index 0000000..8b67787 --- /dev/null +++ b/deploy/aktienbot/.env.bot @@ -0,0 +1,6 @@ +BOT_API_KEY= +NEWS_API_KEY= +SECRET_KEY= + +BOT_EMAIL= +BOT_PASSWORD= \ No newline at end of file diff --git a/deploy/aktienbot/docker-compose.yml b/deploy/aktienbot/docker-compose.yml new file mode 100644 index 0000000..46aee48 --- /dev/null +++ b/deploy/aktienbot/docker-compose.yml @@ -0,0 +1,62 @@ +version: '3.7' + +services: + aktienbot_fe: + image: registry.flokaiser.com/aktienbot/frontend + labels: + traefik.enable: 'true' + traefik.http.routers.aktienbot_fe.rule: Host(`gruppe1.testsites.info`) && !PathPrefix(`/api`) && !PathPrefix(`/phpmyadmin`) && !PathPrefix(`/portainer`) + traefik.http.routers.aktienbot_fe.middlewares: secHeaders@file + traefik.http.routers.aktienbot_fe.priority: 40 + traefik.http.routers.aktienbot_fe.tls: true + traefik.http.routers.aktienbot_fe.tls.certresolver: myresolver + + aktienbot_api: + image: registry.flokaiser.com/aktienbot/api + labels: + traefik.enable: 'true' + traefik.http.routers.aktienbot_api.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/api`) + traefik.http.routers.aktienbot_api.middlewares: secHeaders@file + traefik.http.routers.aktienbot_api.priority: 50 + traefik.http.routers.aktienbot_api.tls: true + traefik.http.routers.aktienbot_api.tls.certresolver: myresolver + depends_on: + - mariadb + env_file: + - ${PWD}/.env.api + + aktienbot_bot: + image: registry.flokaiser.com/aktienbot/bot + env_file: + - ${PWD}/.env.bot + + mariadb: + image: mariadb + volumes: + - mariadb_data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=sBvKtMY7ej9*dETatTtk#uRd5f*5wJYovfdDJDa& + + phpmyadmin: + image: phpmyadmin + environment: + - PMA_HOST=mariadb + - PMA_ABSOLUTE_URI=https://gruppe1.testsites.info/phpmyadmin/ + labels: + traefik.enable: true + traefik.http.routers.phpmyadmin.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/phpmyadmin`) + traefik.http.routers.phpmyadmin.middlewares: secHeaders@file,strip_phpmyadmin + traefik.http.routers.phpmyadmin.priority: 50 + traefik.http.routers.phpmyadmin.tls: true + traefik.http.routers.phpmyadmin.tls.certresolver: myresolver + + traefik.http.middlewares.strip_phpmyadmin.stripprefix.prefixes: /phpmyadmin + +networks: + default: + external: + name: net + +volumes: + portainer_data: + mariadb_data: diff --git a/deploy/base/.env b/deploy/base/.env new file mode 100644 index 0000000..932c2e2 --- /dev/null +++ b/deploy/base/.env @@ -0,0 +1,3 @@ +WATCHTOWER_SCHEDULE=0 5 3 * * * +WATCHTOWER_ROLLING_RESTART=true +WATCHTOWER_CLEANUP=true \ No newline at end of file diff --git a/deploy/base/acme.json b/deploy/base/acme.json new file mode 100644 index 0000000..e69de29 diff --git a/deploy/base/docker-compose.yml b/deploy/base/docker-compose.yml new file mode 100644 index 0000000..0191ffa --- /dev/null +++ b/deploy/base/docker-compose.yml @@ -0,0 +1,83 @@ +version: '3' + +services: + traefik: + image: traefik + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${PWD}/traefik.toml:/etc/traefik/traefik.toml + - ${PWD}/traefik-dynamic.toml:/etc/traefik/traefik-dynamic.toml + - ${PWD}/acme.json:/etc/traefik/acme.json + - ${PWD}/access.log:/etc/traefik/access.log + - ${PWD}/users:/etc/traefik/users + + goaccess: + image: allinurl/goaccess + command: + - --no-global-config + - --config-file=/srv/data/goaccess.conf + - --num-tests=0 + volumes: + - ${PWD}/access.log:/srv/logs/access.log:ro + - ${PWD}/goaccess.conf:/srv/data/goaccess.conf + - goaccess_data:/srv/data + - goaccess_report:/srv/report + labels: + traefik.enable: true + traefik.http.routers.goaccess.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/goaccess/ws`) + traefik.http.routers.goaccess.priority: 55 + traefik.http.routers.goaccess.middlewares: strip_goaccess,goaccess_auth,secHeaders@file + traefik.http.routers.goaccess.tls: true + traefik.http.routers.goaccess.tls.certresolver: myresolver + + nginx: + image: nginx + volumes: + - goaccess_report:/usr/share/nginx/html + labels: + traefik.enable: true + traefik.http.routers.goaccess_web.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/goaccess`) + traefik.http.routers.goaccess_web.priority: 50 + traefik.http.routers.goaccess_web.middlewares: strip_goaccess,goaccess_auth,secHeaders@file + traefik.http.routers.goaccess_web.tls: true + traefik.http.routers.goaccess_web.tls.certresolver: myresolver + + traefik.http.middlewares.strip_goaccess.stripprefix.prefixes: /goaccess + traefik.http.middlewares.goaccess_auth.basicauth.usersfile: /etc/traefik/users + + portainer: + image: portainer/portainer-ce + labels: + traefik.enable: true + traefik.http.routers.portainer.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/portainer`) + traefik.http.routers.portainer.priority: 50 + traefik.http.services.portainer.loadbalancer.server.port: 9000 + traefik.http.routers.portainer.middlewares: strip_portainer,secHeaders@file + traefik.http.routers.portainer.tls: true + traefik.http.routers.portainer.tls.certresolver: myresolver + + traefik.http.middlewares.strip_portainer.stripprefix.prefixes: /portainer + volumes: + - portainer_data:/data + - /var/run/docker.sock:/var/run/docker.sock + + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /etc/localtime:/etc/localtime:ro + env_file: + - ${PWD}/.env + +networks: + default: + external: + name: net + +volumes: + portainer_data: + goaccess_report: + goaccess_data: \ No newline at end of file diff --git a/deploy/base/goaccess.conf b/deploy/base/goaccess.conf new file mode 100644 index 0000000..e23848e --- /dev/null +++ b/deploy/base/goaccess.conf @@ -0,0 +1,5 @@ +log-format COMMON +log-file /srv/logs/access.log +output /srv/report/index.html +real-time-html true +ws-url wss://gruppe1.testsites.info:443/goaccess/ws diff --git a/deploy/base/traefik-dynamic.toml b/deploy/base/traefik-dynamic.toml new file mode 100644 index 0000000..731644f --- /dev/null +++ b/deploy/base/traefik-dynamic.toml @@ -0,0 +1,22 @@ +[tls.options] + [tls.options.default] + minVersion = "VersionTLS12" + cipherSuites = [ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ] + curvePreferences = [ "CurveP521", "CurveP384" ] + sniStrict = true + +[http.middlewares.secHeaders.headers] + browserXssFilter = true + contentTypeNosniff = true + frameDeny = true + stsIncludeSubdomains = true + stsPreload = true + stsSeconds = 31_536_000 + customFrameOptionsValue = "SAMEORIGIN" diff --git a/deploy/base/traefik.toml b/deploy/base/traefik.toml new file mode 100644 index 0000000..d9c0177 --- /dev/null +++ b/deploy/base/traefik.toml @@ -0,0 +1,44 @@ +[log] + level = "INFO" + +[accessLog] + filePath = "/etc/traefik/access.log" + +[entryPoints] + [entryPoints.web] + address = ":80" + + [entryPoints.web.forwardedHeaders] + insecure = true + + [entryPoints.web.http] + [entryPoints.web.http.redirections] + [entryPoints.web.http.redirections.entryPoint] + to = "web-secure" + scheme = "https" + + [entryPoints.web-secure] + address = ":443" + + [entryPoints.web-secure.forwardedHeaders] + insecure = true + + [entryPoints.websecure.http] + middlewares = ["secHeaders@file"] + +[api] + dashboard = true + insecure = true + +[providers.docker] + watch = true + exposedByDefault = false + +[providers.file] + filename = "/etc/traefik/traefik-dynamic.toml" + +[certificatesResolvers.myresolver.acme] + email = "inf20155@lehre.dhbw-stuttgart.de" + storage = "/etc/traefik/acme.json" + [certificatesResolvers.myresolver.acme.httpChallenge] + entryPoint = "web" \ No newline at end of file diff --git a/deploy/base/users b/deploy/base/users new file mode 100644 index 0000000..03bcafb --- /dev/null +++ b/deploy/base/users @@ -0,0 +1 @@ +# Create user by using ```htpasswd -nb user password``` and append output to this file. \ No newline at end of file diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 0000000..80c8665 --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,31 @@ +# Dokumentation + +## Swagger Documentation +Visit https://gruppe1.testsites.info/api/docs + +## API +- `api/openapi.json` + - OpenAPI-Dokumentation + - von APIFlask generiert + - kann in Postman importiert werden +- `api/postman.json` + - Postman Collection + +## Database +- `database/structure_database.pdf` + - Relationales Modell der Datenbank +- `database/structure_database.uxf` + - Relationales Modell der Datenbank + - Source Datei des Modells + +## Requirements +- `requirements/2022-03-15 Anforderungen an Projekt.pdf` + - Anforderungen an dieses Projekt + +## Role-model +Function|Admin|No Admin| +|---|---|---| +|Delete user|yes|only current user| +|Update users|yes|only current user| +|Set admin state|yes|no| +|Show all users|yes|no| diff --git a/documentation/api/openapi.json b/documentation/api/openapi.json new file mode 100644 index 0000000..6b0c300 --- /dev/null +++ b/documentation/api/openapi.json @@ -0,0 +1,1404 @@ +{ + "components": { + "schemas": { + "AdminData": { + "properties": { + "admin": { + "type": "boolean" + }, + "email": { + "format": "email", + "type": "string" + } + }, + "type": "object" + }, + "CronData": { + "properties": { + "cron": { + "type": "string" + } + }, + "type": "object" + }, + "DeleteSuccessful": { + "properties": {}, + "type": "object" + }, + "DeleteUser": { + "properties": { + "email": { + "format": "email", + "type": "string" + } + }, + "type": "object" + }, + "HTTPError": { + "properties": { + "detail": { + "type": "object" + }, + "message": { + "type": "string" + } + }, + "type": "object" + }, + "Keyword": { + "properties": { + "keyword": { + "type": "string" + } + }, + "type": "object" + }, + "KeywordResponse": { + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "keyword": { + "type": "string" + }, + "s_id": { + "type": "integer" + } + }, + "type": "object" + }, + "LoginData": { + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "password": { + "type": "string" + } + }, + "type": "object" + }, + "PortfolioResponse": { + "properties": { + "comment": { + "type": "string" + }, + "count": { + "type": "integer" + }, + "isin": { + "type": "string" + }, + "last_transaction": { + "type": "string" + } + }, + "type": "object" + }, + "RegisterData": { + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "Symbol": { + "properties": { + "comment": { + "type": "string" + }, + "isin": { + "type": "string" + } + }, + "type": "object" + }, + "SymbolPrice": { + "properties": { + "isin": { + "type": "string" + }, + "price": { + "type": "number" + }, + "time": { + "pattern": "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z", + "type": "string" + } + }, + "type": "object" + }, + "SymbolRemove": { + "properties": { + "isin": { + "type": "string" + } + }, + "type": "object" + }, + "SymbolResponse": { + "properties": { + "comment": { + "type": "string" + }, + "email": { + "format": "email", + "type": "string" + }, + "isin": { + "type": "string" + }, + "s_id": { + "type": "integer" + } + }, + "type": "object" + }, + "TelegramId": { + "properties": { + "telegram_user_id": { + "type": "string" + } + }, + "type": "object" + }, + "Token": { + "properties": { + "token": { + "type": "string" + } + }, + "type": "object" + }, + "Transaction": { + "properties": { + "comment": { + "type": "string" + }, + "count": { + "type": "integer" + }, + "isin": { + "type": "string" + }, + "price": { + "type": "number" + }, + "time": { + "pattern": "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z", + "type": "string" + } + }, + "type": "object" + }, + "TransactionResponse": { + "properties": { + "comment": { + "type": "string" + }, + "count": { + "type": "integer" + }, + "email": { + "format": "email", + "type": "string" + }, + "isin": { + "type": "string" + }, + "price": { + "type": "number" + }, + "time": { + "type": "string" + } + }, + "type": "object" + }, + "UpdateUserData": { + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "Users": { + "properties": { + "admin": { + "type": "boolean" + }, + "cron": { + "type": "string" + }, + "email": { + "format": "email", + "type": "string" + }, + "password": { + "type": "string" + }, + "telegram_user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "ValidationError": { + "properties": { + "detail": { + "properties": { + "": { + "properties": { + "": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "BearerAuth": { + "scheme": "Bearer", + "type": "http" + } + } + }, + "info": { + "title": "APIFlask", + "version": "0.1.0" + }, + "openapi": "3.0.3", + "paths": { + "/api/keyword": { + "delete": { + "description": "Removes existing keyword for current user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Keyword" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/DeleteSuccessful" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Removes existing keyword", + "tags": [ + "Keyword" + ] + }, + "post": { + "description": "Adds new keyword for current user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Keyword" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/KeywordResponse" + }, + "type": "array" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Add new keyword", + "tags": [ + "Keyword" + ] + } + }, + "/api/keywords": { + "get": { + "description": "Returns all keywords for current user", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/KeywordResponse" + }, + "type": "array" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Returns all keywords", + "tags": [ + "Keyword" + ] + } + }, + "/api/portfolio": { + "get": { + "description": "Returns all shares of current user", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/PortfolioResponse" + }, + "type": "array" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Returns portfolio", + "tags": [ + "Portfolio" + ] + } + }, + "/api/share": { + "delete": { + "description": "Removes existing symbol for current user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SymbolRemove" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/DeleteSuccessful" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Removes existing symbol", + "tags": [ + "Share" + ] + }, + "post": { + "description": "Adds new symbol for current user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Symbol" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/SymbolResponse" + }, + "type": "array" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Add new symbol", + "tags": [ + "Share" + ] + } + }, + "/api/shares": { + "get": { + "description": "Returns all symbols for current user", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/SymbolResponse" + }, + "type": "array" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Returns all symbols", + "tags": [ + "Share" + ] + } + }, + "/api/symbol": { + "post": { + "description": "Adds new price to database", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SymbolPrice" + } + } + } + }, + "responses": { + "204": { + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Adds new price for isin", + "tags": [ + "Share_Price" + ] + } + }, + "/api/symbols": { + "get": { + "description": "Returns all transaction symbols for all users", + "parameters": [], + "responses": { + "204": { + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Returns all transaction symbols", + "tags": [ + "Share_Price" + ] + } + }, + "/api/telegram": { + "post": { + "description": "Connects telegram user id to user account", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TelegramId" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/Users" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Connects telegram user id", + "tags": [ + "Telegram" + ] + } + }, + "/api/transaction": { + "post": { + "description": "Adds new transaction for current user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Transaction" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/TransactionResponse" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Adds new transaction", + "tags": [ + "Transaction" + ] + } + }, + "/api/transactions": { + "get": { + "description": "Returns all transactions for current user", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/Transaction" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Returns all transactions", + "tags": [ + "Transaction" + ] + } + }, + "/api/user": { + "delete": { + "description": "Deletes user by username", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteUser" + } + } + } + }, + "responses": { + "204": { + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Delete user", + "tags": [ + "Users" + ] + }, + "get": { + "description": "Returns current user", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/Users" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Get current user", + "tags": [ + "Users" + ] + }, + "put": { + "description": "Changes password and/or username of current user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserData" + } + } + } + }, + "responses": { + "204": { + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Update user", + "tags": [ + "Users" + ] + } + }, + "/api/user/login": { + "post": { + "description": "Returns jwt token if username and password match, otherwise returns error", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginData" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/Token" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + } + }, + "summary": "Login", + "tags": [ + "Users" + ] + } + }, + "/api/user/register": { + "post": { + "description": "Registers user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterData" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "$ref": "#/components/schemas/Users" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + } + }, + "summary": "Register", + "tags": [ + "Users" + ] + } + }, + "/api/user/setAdmin": { + "put": { + "description": "Set admin state of specified user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminData" + } + } + } + }, + "responses": { + "204": { + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Set user admin state", + "tags": [ + "Users" + ] + } + }, + "/api/user/setCron": { + "put": { + "description": "Set update cron of specified user", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CronData" + } + } + } + }, + "responses": { + "204": { + "description": "Successful response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + }, + "description": "Validation error" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Set update cron", + "tags": [ + "Users" + ] + } + }, + "/api/users": { + "get": { + "description": "Returns all existing users as array", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/Users" + }, + "type": "array" + }, + "status": { + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPError" + } + } + }, + "description": "Authentication error" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Get all users", + "tags": [ + "Users" + ] + } + } + }, + "tags": [ + { + "name": "Keyword" + }, + { + "name": "Share" + }, + { + "name": "Share_Price" + }, + { + "name": "Transaction" + }, + { + "name": "Portfolio" + }, + { + "name": "Users" + }, + { + "name": "Telegram" + } + ] +} diff --git a/documentation/api/postman.json b/documentation/api/postman.json index f8260ec..994ca6e 100644 --- a/documentation/api/postman.json +++ b/documentation/api/postman.json @@ -1,126 +1,469 @@ { "info": { - "_postman_id": "67da7d20-d7df-4ad3-9289-a6e40b6cd2ec", - "name": "AktienBot", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, + "_postman_id": "ab12ad1f-aaf8-44c2-80de-bd4e1ec72c4b", + "name": "APIFlask", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, "item": [ { - "name": "User", + "name": "Keyword", "item": [ { - "name": "/api/register", + "name": "Removes existing keyword", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/keyword", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keyword" + ] + }, + "description": "Removes existing keyword for current user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/keyword", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keyword" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {},\n \"status\": -6906184,\n \"text\": \"velit ad reprehenderit quis\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/keyword", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keyword" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/keyword", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keyword" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Add new keyword", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", - "options": { - "raw": { - "language": "json" - } + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" } - }, + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{BASE_URL}}/api/register", + "raw": "{{baseUrl}}/api/keyword", "host": [ - "{{BASE_URL}}" + "{{baseUrl}}" ], "path": [ "api", - "register" + "keyword" ] - } - }, - "response": [] - }, - { - "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" - ] - } + "description": "Adds new keyword for current user" }, - "response": [] + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/keyword", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keyword" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"email\": \"4zcg8Ae@mFFZbwrszRgeKi.bmly\",\n \"keyword\": \"laboris\",\n \"s_id\": -29368814\n },\n {\n \"email\": \"WcrLS@cPNpWwtFqRDxUEumdjOsPLukcg.lmxg\",\n \"keyword\": \"veniam\",\n \"s_id\": 58969388\n }\n ],\n \"status\": -62012539,\n \"text\": \"ut incididunt sunt\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/keyword", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keyword" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"keyword\": \"veniam pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/keyword", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keyword" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] }, { - "name": "/api/logout", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, + "name": "Returns all keywords", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"Username\",\n \"password\": \"Password\"\n}", - "options": { - "raw": { - "language": "json" - } + "header": [ + { + "key": "Accept", + "value": "application/json" } - }, + ], "url": { - "raw": "{{BASE_URL}}/api/logout", + "raw": "{{baseUrl}}/api/keywords", "host": [ - "{{BASE_URL}}" + "{{baseUrl}}" ], "path": [ "api", - "logout" + "keywords" ] - } - }, - "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" - ] - } + "description": "Returns all keywords for current user" }, - "response": [] + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/keywords", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keywords" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"email\": \"4zcg8Ae@mFFZbwrszRgeKi.bmly\",\n \"keyword\": \"laboris\",\n \"s_id\": -29368814\n },\n {\n \"email\": \"WcrLS@cPNpWwtFqRDxUEumdjOsPLukcg.lmxg\",\n \"keyword\": \"veniam\",\n \"s_id\": 58969388\n }\n ],\n \"status\": -62012539,\n \"text\": \"ut incididunt sunt\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/keywords", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "keywords" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] } ] }, @@ -128,210 +471,1031 @@ "name": "Share", "item": [ { - "name": "/api/shares", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, + "name": "Removes existing symbol", "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"quis pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{BASE_URL}}/api/shares", + "raw": "{{baseUrl}}/api/share", "host": [ - "{{BASE_URL}}" + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + }, + "description": "Removes existing symbol for current user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"quis pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/share", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {},\n \"status\": -6906184,\n \"text\": \"velit ad reprehenderit quis\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"quis pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/share", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"quis pariatur\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/share", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Add new symbol", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"comment\": \"irure adipisicing consectetur\",\n \"isin\": \"non irure ea labore\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/share", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + }, + "description": "Adds new symbol for current user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"comment\": \"irure adipisicing consectetur\",\n \"isin\": \"non irure ea labore\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/share", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"comment\": \"aute eu id\",\n \"email\": \"7Wq8YgGHE@T.phx\",\n \"isin\": \"amet anim Duis proident dolore\",\n \"s_id\": 87738546\n },\n {\n \"comment\": \"ut mollit\",\n \"email\": \"MT8jKX-7mq@gxctz.tskk\",\n \"isin\": \"in nost\",\n \"s_id\": -16197478\n }\n ],\n \"status\": -39445086,\n \"text\": \"aliqua consequat aute officia\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"comment\": \"irure adipisicing consectetur\",\n \"isin\": \"non irure ea labore\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/share", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"comment\": \"irure adipisicing consectetur\",\n \"isin\": \"non irure ea labore\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/share", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "share" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Returns all symbols", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/shares", + "host": [ + "{{baseUrl}}" ], "path": [ "api", "shares" ] - } + }, + "description": "Returns all symbols for current user" }, - "response": [] - }, - { - "name": "/api/share", + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/shares", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "shares" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"comment\": \"aute eu id\",\n \"email\": \"7Wq8YgGHE@T.phx\",\n \"isin\": \"amet anim Duis proident dolore\",\n \"s_id\": 87738546\n },\n {\n \"comment\": \"ut mollit\",\n \"email\": \"MT8jKX-7mq@gxctz.tskk\",\n \"isin\": \"in nost\",\n \"s_id\": -16197478\n }\n ],\n \"status\": -39445086,\n \"text\": \"aliqua consequat aute officia\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/shares", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "shares" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + } + ] + }, + { + "name": "Share_Price", + "item": [ + { + "name": "Adds new price for isin", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"consequat ut elit incididunt\",\n \"price\": 8596518.682177305,\n \"time\": \"6953-98-62T41:25:08.545Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/symbol", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "symbol" + ] + }, + "description": "Adds new price to database" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"consequat ut elit incididunt\",\n \"price\": 8596518.682177305,\n \"time\": \"6953-98-62T41:25:08.545Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/symbol", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "symbol" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "cookie": [], + "body": "" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"consequat ut elit incididunt\",\n \"price\": 8596518.682177305,\n \"time\": \"6953-98-62T41:25:08.545Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/symbol", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "symbol" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"isin\": \"consequat ut elit incididunt\",\n \"price\": 8596518.682177305,\n \"time\": \"6953-98-62T41:25:08.545Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/symbol", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "symbol" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Returns all transaction symbols", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/symbols", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "symbols" + ] + }, + "description": "Returns all transaction symbols for all users" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/symbols", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "symbols" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "cookie": [], + "body": "" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/symbols", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "symbols" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + } + ] + }, + { + "name": "Transaction", + "item": [ + { + "name": "Adds new transaction", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"symbol\": \"DTEGY\"\n}", - "options": { - "raw": { - "language": "json" - } + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/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" - } - } - }, + "mode": "raw", + "raw": "{\n \"comment\": \"non nulla\",\n \"count\": 96442090,\n \"isin\": \"non esse\",\n \"price\": -17020893.639093384,\n \"time\": \"9660-60-73T53:64:02.637Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{BASE_URL}}/api/share", + "raw": "{{baseUrl}}/api/transaction", "host": [ - "{{BASE_URL}}" - ], - "path": [ - "api", - "share" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Keyword", - "item": [ - { - "name": "/api/keywords", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{BASE_URL}}/api/keywords", - "host": [ - "{{BASE_URL}}" - ], - "path": [ - "api", - "keywords" - ] - } - }, - "response": [] - }, - { - "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": [] - }, - { - "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": [] - } - ] - }, - { - "name": "Transactions", - "item": [ - { - "name": "/api/transactions", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{BASE_URL}}/api/transactions", - "host": [ - "{{BASE_URL}}" - ], - "path": [ - "api", - "transactions" - ] - } - }, - "response": [] - }, - { - "name": "/api/transaction", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"symbol\": \"DTEGY\",\n \"time\": \"2021-03-14T18:08:44.625Z\",\n \"count\": 1,\n \"price\": 10.0\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{BASE_URL}}/api/transaction", - "host": [ - "{{BASE_URL}}" + "{{baseUrl}}" ], "path": [ "api", "transaction" ] - } + }, + "description": "Adds new transaction for current user" }, - "response": [] + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"comment\": \"non nulla\",\n \"count\": 96442090,\n \"isin\": \"non esse\",\n \"price\": -17020893.639093384,\n \"time\": \"9660-60-73T53:64:02.637Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/transaction", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "transaction" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"comment\": \"ex cillum ullamco officia\",\n \"count\": -23493756,\n \"email\": \"fmL6Vhy@daFGKga.aton\",\n \"isin\": \"aliquip enim sint\",\n \"price\": -46738573.8885945,\n \"time\": \"aute enim\"\n },\n \"status\": -46966272,\n \"text\": \"adipisicing qui amet nulla\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"comment\": \"non nulla\",\n \"count\": 96442090,\n \"isin\": \"non esse\",\n \"price\": -17020893.639093384,\n \"time\": \"9660-60-73T53:64:02.637Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/transaction", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "transaction" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"comment\": \"non nulla\",\n \"count\": 96442090,\n \"isin\": \"non esse\",\n \"price\": -17020893.639093384,\n \"time\": \"9660-60-73T53:64:02.637Z\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/transaction", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "transaction" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Returns all transactions", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/transactions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "transactions" + ] + }, + "description": "Returns all transactions for current user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/transactions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "transactions" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"comment\": \"ex ipsum adipisicing dolor\",\n \"count\": 46881527,\n \"isin\": \"ad eiusmod\",\n \"price\": -11144262.12990503,\n \"time\": \"5529-43-74T12:79:94.205Z\"\n },\n \"status\": 25470796,\n \"text\": \"exercitation et est\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/transactions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "transactions" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] } ] }, @@ -339,61 +1503,1455 @@ "name": "Portfolio", "item": [ { - "name": "/api/portfolio", + "name": "Returns portfolio", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, "method": "GET", - "header": [], + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], "url": { - "raw": "{{BASE_URL}}/api/portfolio", + "raw": "{{baseUrl}}/api/portfolio", "host": [ - "{{BASE_URL}}" + "{{baseUrl}}" ], "path": [ "api", "portfolio" ] - } + }, + "description": "Returns all shares of current user" }, - "response": [] + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/portfolio", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "portfolio" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"comment\": \"sint quis ut\",\n \"count\": 54930744,\n \"isin\": \"labore cillum\",\n \"last_transaction\": \"id cillum non\"\n },\n {\n \"comment\": \"cillum ut ipsum\",\n \"count\": 44991486,\n \"isin\": \"in in consectetur velit\",\n \"last_transaction\": \"anim veniam\"\n }\n ],\n \"status\": 87593619,\n \"text\": \"non culpa occaecat\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/portfolio", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "portfolio" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Delete user", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"oG8z5swOjkbst@kinVwpTMhsrUUCewsWdTkNXO.pqv\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + }, + "description": "Deletes user by username" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"oG8z5swOjkbst@kinVwpTMhsrUUCewsWdTkNXO.pqv\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "cookie": [], + "body": "" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"oG8z5swOjkbst@kinVwpTMhsrUUCewsWdTkNXO.pqv\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "DELETE", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"oG8z5swOjkbst@kinVwpTMhsrUUCewsWdTkNXO.pqv\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Get current user", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + }, + "description": "Returns current user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"admin\": true,\n \"cron\": \"id in Excepteur\",\n \"email\": \"E3rj@zUSyCWAiewlz.vrx\",\n \"password\": \"sunt pariatur\",\n \"telegram_user_id\": \"elit\",\n \"username\": \"mollit incididunt nostrud\"\n },\n \"status\": -87469860,\n \"text\": \"ullamco in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Update user", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"password\": \"ut\",\n \"username\": \"ullamco amet\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + }, + "description": "Changes password and/or username of current user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"password\": \"ut\",\n \"username\": \"ullamco amet\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "cookie": [], + "body": "" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"password\": \"ut\",\n \"username\": \"ullamco amet\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"password\": \"ut\",\n \"username\": \"ullamco amet\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"GfEubxoHYHPPc4@PXsBSXusLxIotbYTjEAaQnAVHLDz.jv\",\n \"password\": \"Duis ut\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/login", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "login" + ] + }, + "description": "Returns jwt token if username and password match, otherwise returns error" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"GfEubxoHYHPPc4@PXsBSXusLxIotbYTjEAaQnAVHLDz.jv\",\n \"password\": \"Duis ut\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/login", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "login" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"token\": \"non\"\n },\n \"status\": 92819984,\n \"text\": \"consectetur elit esse non\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"GfEubxoHYHPPc4@PXsBSXusLxIotbYTjEAaQnAVHLDz.jv\",\n \"password\": \"Duis ut\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/login", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "login" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + } + ] + }, + { + "name": "Register", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"3zVMb95E@jZETuCbGKKbkmiNrTNNfdpTPQdkBJNbzD.wicb\",\n \"password\": \"eiusmod cillum\",\n \"username\": \"enim\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/register", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "register" + ] + }, + "description": "Registers user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"3zVMb95E@jZETuCbGKKbkmiNrTNNfdpTPQdkBJNbzD.wicb\",\n \"password\": \"eiusmod cillum\",\n \"username\": \"enim\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/register", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "register" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"admin\": true,\n \"cron\": \"id in Excepteur\",\n \"email\": \"E3rj@zUSyCWAiewlz.vrx\",\n \"password\": \"sunt pariatur\",\n \"telegram_user_id\": \"elit\",\n \"username\": \"mollit incididunt nostrud\"\n },\n \"status\": -87469860,\n \"text\": \"ullamco in\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"3zVMb95E@jZETuCbGKKbkmiNrTNNfdpTPQdkBJNbzD.wicb\",\n \"password\": \"eiusmod cillum\",\n \"username\": \"enim\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/register", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "register" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + } + ] + }, + { + "name": "Set user admin state", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"admin\": false,\n \"email\": \"3lnXNx5@NhoXHfuhjTiz.bgre\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setAdmin", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setAdmin" + ] + }, + "description": "Set admin state of specified user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"admin\": false,\n \"email\": \"3lnXNx5@NhoXHfuhjTiz.bgre\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setAdmin", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setAdmin" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "cookie": [], + "body": "" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"admin\": false,\n \"email\": \"3lnXNx5@NhoXHfuhjTiz.bgre\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setAdmin", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setAdmin" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"admin\": false,\n \"email\": \"3lnXNx5@NhoXHfuhjTiz.bgre\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setAdmin", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setAdmin" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Set update cron", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"cron\": \"dolor\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setCron", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setCron" + ] + }, + "description": "Set update cron of specified user" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"cron\": \"dolor\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setCron", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setCron" + ] + } + }, + "status": "No Content", + "code": 204, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "text/plain" + } + ], + "cookie": [], + "body": "" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"cron\": \"dolor\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setCron", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setCron" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"in esse ut consectetur\",\n \"dolor dolore\"\n ]\n }\n },\n \"message\": \"tempor enim magna in\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "PUT", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"cron\": \"dolor\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/user/setCron", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "user", + "setCron" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + }, + { + "name": "Get all users", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "users" + ] + }, + "description": "Returns all existing users as array" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "users" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"admin\": false,\n \"cron\": \"cupidatat nostrud\",\n \"email\": \"udtQkVtNcxr@OJMRrDDwMlfw.udu\",\n \"password\": \"dolore consectetur quis n\",\n \"telegram_user_id\": \"sint in\",\n \"username\": \"in minim ea labo\"\n },\n {\n \"admin\": false,\n \"cron\": \"enim exercitation laboris voluptate aute\",\n \"email\": \"jQyQftlI@zOMiqukOfCTGwRN.bp\",\n \"password\": \"non minim tempor laboris exercitation\",\n \"telegram_user_id\": \"ipsum Excepteur adipisicing\",\n \"username\": \"ad\"\n }\n ],\n \"status\": -85994498,\n \"text\": \"pariatur nostrud cupidatat\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "GET", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "url": { + "raw": "{{baseUrl}}/api/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "users" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] + } + ] + }, + { + "name": "Telegram", + "item": [ + { + "name": "Connects telegram user id", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"telegram_user_id\": \"adipisicing ut occaecat\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/telegram", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "telegram" + ] + }, + "description": "Connects telegram user id to user account" + }, + "response": [ + { + "name": "Successful response", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"telegram_user_id\": \"adipisicing ut occaecat\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/telegram", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "telegram" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"admin\": true,\n \"cron\": \"id in Excepteur\",\n \"email\": \"E3rj@zUSyCWAiewlz.vrx\",\n \"password\": \"sunt pariatur\",\n \"telegram_user_id\": \"elit\",\n \"username\": \"mollit incididunt nostrud\"\n },\n \"status\": -87469860,\n \"text\": \"ullamco in\"\n}" + }, + { + "name": "Validation error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"telegram_user_id\": \"adipisicing ut occaecat\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/telegram", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "telegram" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {\n \"\": {\n \"\": [\n \"tempor consectetur sint adipisicing\",\n \"occaecat laborum consectetur\"\n ]\n }\n },\n \"message\": \"dolor est officia\"\n}" + }, + { + "name": "Authentication error", + "originalRequest": { + "method": "POST", + "header": [ + { + "description": "Added as a part of security scheme: bearer", + "key": "Authorization", + "value": "Bearer " + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"telegram_user_id\": \"adipisicing ut occaecat\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/api/telegram", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "telegram" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": {},\n \"message\": \"non elit\"\n}" + } + ] } ] } ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IlVzZXJuYW1lIiwiZXhwIjoxNjQ3Mjk5MzIxfQ.5UoHi9Zu6p9szSVKK2_1Ln2uru4RVTGQl0MyHDB4sqg", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], "variable": [ - { - "key": "BASE_URL", - "value": "https://aktienbot.flokaiser.com/", - "type": "default" - } + { + "key": "baseUrl", + "value": "/", + "type": "string" + } ] } \ No newline at end of file diff --git a/documentation/database/README.md b/documentation/database/README.md deleted file mode 100644 index 26568bd..0000000 --- a/documentation/database/README.md +++ /dev/null @@ -1 +0,0 @@ -This is the database folder diff --git a/documentation/database/structure_database.pdf b/documentation/database/structure_database.pdf deleted file mode 100644 index fdd0a10..0000000 Binary files a/documentation/database/structure_database.pdf and /dev/null differ diff --git a/documentation/database/structure_database.png b/documentation/database/structure_database.png new file mode 100644 index 0000000..4265215 Binary files /dev/null and b/documentation/database/structure_database.png differ diff --git a/documentation/database/structure_database.uxf b/documentation/database/structure_database.uxf index 6702c2a..afdf01e 100644 --- a/documentation/database/structure_database.uxf +++ b/documentation/database/structure_database.uxf @@ -1,105 +1,38 @@ - - - 10 - - UMLClass - - 580 - 320 - 210 - 80 - - Stichwort +10Database V0.1UMLClass70536021080keywords -- -PK: s_id -FK: username -Stichwort - - - - UMLClass - - 580 - 100 - 210 - 130 - - User +PK: s_id int(11) +FK: email varchar(255) +keyword varchar(255)UMLClass425240210130users -- -PK: username -password -user_id -telegramname -Rolle - - - - - UMLClass - - 930 - 80 - 210 - 90 - - Aktie +PK: email varchar(255) +password varchar(255) +username varchar(255) +telegram_user_id varchar(255) +admin tinyint(1) +cron varchar(20) +UMLClass70515021090shares -- -PK: a_id -FK: username -Aktiensymbol - - - - - UMLClass - - 200 - 320 - 210 - 130 - - Transaktion +PK: a_id int(11) +FK: email varchar(255) +isin varchar(255) +comment varchar(255)UMLClass135150210130transaction -- -PK: t_id -FK: username -FK: Aktiensymbol -Zeitpunkt -Anzahl(+/-) -Preis(+/-) - - - - - Relation - - 780 - 120 - 170 - 30 - - lt=<- - 150.0;10.0;10.0;10.0 - - - Relation - - 400 - 120 - 200 - 260 - - lt=<- - 10.0;240.0;180.0;10.0 - - - Relation - - 510 - 200 - 90 - 190 - - lt=<- - 70.0;170.0;10.0;10.0 - - +PK: t_id int(11) +FK: email varchar(255) +isin varchar(255) +comment varchar(255) +time datetime +count int(11) +price float +Relation335190110100lt=<<- +group=110;10;60;10;60;80;90;80Relation625190100100lt=<<-80;10;50;10;50;80;10;80Relation625260100160lt=<<-80;140;50;140;50;10;10;10UMLClass14041010030Database V0.1UMLClass70515021090shares +-- +PK: a_id int(11) +FK: email varchar(255) +isin varchar(255) +comment varchar(255)UMLClass14030021090share_price +-- +PK: id int(11) +isin varchar(255) +price float +date datetime \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index f298e1e..d487766 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,19 +1,29 @@ FROM node:latest as build +# Change to the project directory WORKDIR /usr/local/app +# Copy the project files to the container COPY frontend /usr/local/app/ +# Install dependencies RUN npm install RUN npm run build -RUN ls /usr/local/app/dist - - FROM nginx:latest +# Copy the project files to the container COPY --from=build /usr/local/app/dist/aktienbot /usr/share/nginx/html +# Copy configuration files +COPY frontend/deploy/nginx.conf /etc/nginx +COPY frontend/deploy deploy/ + +# Change file permissions +RUN chmod +x ./deploy/healthcheck.sh + +# set healthcheck HEALTHCHECK --interval=15s --timeout=2s CMD ["./deploy/healthcheck.sh"] +# Expose webserver port EXPOSE 80 diff --git a/frontend/README.md b/frontend/README.md index 0a4c386..a0a4b06 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -25,3 +25,4 @@ Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To u ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. + diff --git a/frontend/deploy/healthcheck.sh b/frontend/deploy/healthcheck.sh index 390f507..fffa122 100644 --- a/frontend/deploy/healthcheck.sh +++ b/frontend/deploy/healthcheck.sh @@ -1,2 +1,3 @@ #!/usr/bin/env sh + curl -s http://localhost:80/ -o /dev/null || exit 1 diff --git a/frontend/deploy/nginx.conf b/frontend/deploy/nginx.conf new file mode 100644 index 0000000..f1595bf --- /dev/null +++ b/frontend/deploy/nginx.conf @@ -0,0 +1,34 @@ +events { + worker_connections 1024; ## Default: 1024 +} + +http { + ## use mime types + include /etc/nginx/mime.types; + server { + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html; + ## without this our .css are not loaded + try_files $uri $uri/ /index.html?$query_string; + } + } + + ## enable gzip compression + gzip on; + gzip_vary on; + gzip_min_length 256; + gzip_proxied any; + + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/x-javascript + application/xml + application/json + application/ld+json; +} diff --git a/frontend/src/app/Services/auth.service.ts b/frontend/src/app/Services/auth.service.ts index a4dff21..c011e2c 100644 --- a/frontend/src/app/Services/auth.service.ts +++ b/frontend/src/app/Services/auth.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; const AUTH_API = 'https://gruppe1.testsites.info/api/user'; + const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), }; diff --git a/telegram_bot/.env.example b/telegram_bot/.env.example new file mode 100644 index 0000000..a36e6ec --- /dev/null +++ b/telegram_bot/.env.example @@ -0,0 +1,12 @@ +# Telegram bot api key +BOT_API_KEY= + +# News api key +NEWS_API_KEY= + +# Flask secret key +SECRET_KEY= + +# bot credentials +BOT_EMAIL= +BOT_PASSWORD= \ No newline at end of file diff --git a/telegram_bot/Dockerfile b/telegram_bot/Dockerfile index dc51065..ca7ffb7 100644 --- a/telegram_bot/Dockerfile +++ b/telegram_bot/Dockerfile @@ -1,17 +1,17 @@ FROM python:3.10-slim +# Change the working directory to the root of the project WORKDIR /srv/flask_app +# Install the dependencies COPY telegram_bot/requirements.txt /srv/flask_app/ - RUN pip install -r requirements.txt --src /usr/local/src --no-warn-script-location +# Copy the source code to the working directory COPY telegram_bot /srv/flask_app + +# Change file permissions 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"] +# Run the app +CMD ["./deploy/start.sh"] \ No newline at end of file diff --git a/telegram_bot/README.md b/telegram_bot/README.md new file mode 100644 index 0000000..05ac44d --- /dev/null +++ b/telegram_bot/README.md @@ -0,0 +1,33 @@ +# Telegram bot + +Aktienbot telegram bot + +## Development +1. Create virtual environment `python -m venv venv env/Scripts/activate` +2. Install requirements `pip install -r telegram_bot/requirements.txt` +3. Set environment variables (see list below) + 1. Use `.env`-file in `api` directory like `.env.example` + 2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`) +4. Run api `python telegram_bot/bot.py` + +## Environment variables +``` + # Telegram bot api key + BOT_API_KEY= + + # News api key + NEWS_API_KEY= +``` + +## Docker +``` +docker run -d \ + --name aktienbot_bot \ + --hostname aktienbot_bot \ + --publish 80:80 \ + --env "BOT_API_KEY=" \ + --env "NEWS_API_KEY=" \ + --restart unless-stopped \ + registry.flokaiser.com/aktienbot/bot:latest +``` +or load environment variables from file by using `--env-file ` \ No newline at end of file diff --git a/telegram_bot/api_handler.py b/telegram_bot/api_handler.py deleted file mode 100644 index 69c4974..0000000 --- a/telegram_bot/api_handler.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -script for communicating with webservice to get data from database -""" -__author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "16.03.2022" -__version__ = "0.0.1" -__license__ = "None" \ No newline at end of file diff --git a/telegram_bot/api_handling/__init__.py b/telegram_bot/api_handling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram_bot/api_handling/api_handler.py b/telegram_bot/api_handling/api_handler.py new file mode 100644 index 0000000..35aef19 --- /dev/null +++ b/telegram_bot/api_handling/api_handler.py @@ -0,0 +1,374 @@ +""" +script for communicating with webservice to get data from database +""" +__author__ = "Florian Kellermann, Linus Eickhoff" +__date__ = "26.04.2022" +__version__ = "1.0.1" +__license__ = "None" + +#side-dependencies: none +#Work in Progress + +import sys +import os +import requests as r +from croniter import croniter # used for checking cron formatting +from dotenv import load_dotenv + +load_dotenv() # loads environment vars + +# note: for more information about the api visit swagger documentation on https://gruppe1.testsites.info/api/docs#/ + +class API_Handler: + """class for interacting with the api webservice + + Attributes: + db_adress (string): adress of the database + token (string): auth token for api + + Methods: + reauthorize(email, password): set new credentials + get_user_keywords(user_id): gets the keywords of the user + set_keyword(user_id, keyword): sets the keyword of the user + delete_keyword(user_id, keyword): deletes the keyword of the user + get_user_shares(user_id): gets the shares of the user + set_share(user_id, symbol): sets the share of the user + delete_share(user_id, symbol): deletes the share of the user + get_user_transactions(user_id): gets the transactions of the user + set_transaction(user_id, transaction): sets the transaction of the user + get_user_portfolio(user_id): gets the portfolio of the user + set_portfolio(user_id, portfolio): sets the portfolio of the user + delete_portfolio(user_id, portfolio): deletes the portfolio of the user + set_cron_interval(user_id, interval): sets the cron interval of the user + set_admin(email, is_admin): sets the admin status of the user with the given email + """ + + + def __init__(self, db_adress, email, password): + """initializes the API_Handler class + + Args: + db_adress (string): adress of the database + email (string): email of the user + password (string): password of the user + """ + self.db_adress = db_adress + + payload = {'email': email, 'password': password} # credentials for admin account that has all permissions to get and set data (in this case bot account) + with r.Session() as s: # open session + p = s.post(self.db_adress + "/user/login", json=payload) # login to webservice + if p.status_code == 200: + self.token = p.json()["data"]['token'] # store token for further authentication of requests + else: + print("Error: " + str(p.status_code) + " invalid credentials") + self.token = None + + + def reauthorize(self, email, password): # can be used if token expired + """set new credentials + + Args: + email (string): email of the user + password (string): password of the user + """ + payload = {'email': email, 'password': password} + with r.Session() as s: + p = s.post(self.db_adress + "/user/login", json=payload) + if p.status_code == 200: + self.token = p.json()["data"]['token'] + return p.json()["data"]['token'] + else: + self.token = None + return None + + + def get_user(self, user_id, max_retries=10): # max retries are used recursively if the request fails + """gets the shares of the user + + Args: + user_id (int): id of the user + max_retries (int): max retries for the request + + Returns: + json: json of user infos + """ + if max_retries <= 0: + return None + + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} # authorization is bot_token:user_id (user_id is the id of the user you want to get data from) + req = s.get(self.db_adress + "/user", headers=headers) + if(req.status_code == 200): + return req.json()["data"] + + else: + return self.get_user(user_id, max_retries-1) # if request fails try again recursively + + + def get_all_users(self, max_retries=10): + """gets all users + + Args: + max_retries (int): max retries for the request + + Returns: + list: list of users + """ + if max_retries <= 0: + return None + + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token} + req = s.get(self.db_adress + "/users", headers=headers) + if(req.status_code == 200): + return req.json()["data"] + + else: + return self.get_all_users(max_retries-1) + + + def get_user_keywords(self, user_id, max_retries=10): + """gets the keywords of the user + + Args: + user_id (int): id of the user + max_retries (int): max retries for the request + + Returns: + list: list of keywords + """ + if max_retries <= 0: + return None + + keywords = [] + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.get(self.db_adress + "/keywords", headers=headers) + if(req.status_code == 200): + keywords_json = req.json()["data"] + for keyword in keywords_json: # keywords_json is a list of dictionaries + keywords.append(keyword["keyword"]) + + return keywords # will be empty if no keywords are set + + else: + return self.get_user_keywords(user_id, max_retries-1) + + + + def set_keyword(self, user_id, keyword): + """sets the keyword of the user + + Args: + user_id (int): id of the user + keyword (int): keyword of the user + + Returns: + int: status code + """ + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.post(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers) + + return req.status_code + + + def delete_keyword(self, user_id, keyword): + """deletes the keyword of the user + + Args: + user_id (int): id of the user + keyword (string): keyword of the user + + Returns: + int: status code + """ + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.delete(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers) + + return req.status_code + + + def get_user_shares(self, user_id, max_retries=10): + """gets the shares of the user + + Args: + user_id (int): id of the user + max_retries (int): max retries for the request + + Returns: + list: list of shares + """ + if max_retries <= 0: + return None + + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.get(self.db_adress + "/shares", headers=headers) + if(req.status_code == 200): + shares_json = req.json()["data"] + shares = [] + for share in shares_json: + shares.append(share["isin"]) # we only want the isin of the shares + + return shares + + else: + return self.get_user_shares(user_id, max_retries-1) + + + def set_share(self, user_id, isin, comment): + """sets the share of the user + + Args: + user_id (int): id of the user + isin (string): isin of the share + comment (string): comment of the share + + Returns: + int: status code + """ + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.post(self.db_adress + "/share", json={"comment": comment, "isin": isin}, headers=headers) # set share by setting comment and isin, comment can be the real name of the share e.g. "Apple Inc." + return req.status_code + + + def delete_share(self, user_id, isin): + """deletes the share of the user + + Args: + user_id (int): id of the user + symbol (string): symbol of the share + + Returns: + int: status code + """ + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.delete(self.db_adress + "/share", json={"isin": isin}, headers=headers) # to delete a share only the isin is needed because it is unique, shares are not transactions! + return req.status_code + + + def get_user_transactions(self, user_id, max_retries=10): + """gets the transactions of the user + + Args: + user_id (int): id of the user + max_retries (int): max retries for the request + + Returns: + dict: dictionary of transactions + """ + if max_retries <= 0: + return None + + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.get(self.db_adress + "/transactions", headers=headers) + + if req.status_code == 200: + transactions_dict = req.json()["data"] + return transactions_dict + else: + return self.get_user_transactions(user_id, max_retries-1) + + + def set_transaction(self, user_id, comment, isin, count, price, time): + """sets the transaction of the user + + Args: + user_id (int): id of the user + comment (string): comment of the transaction + isin (string): isin of the transaction + count (float): count of the transaction + price (float): price of the transaction + time (string): time of the transaction formatted like e.g. "2011-10-05T14:48:00.000Z" + + Returns: + int: status code + """ + with r.Session() as s: + time = time[:-3] + "Z" # remove last character and add Z to make it a valid date for db + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + transaction = {"comment": str(comment), "count": float(count), "isin": str(isin), "price": float(price), "time": str(time)} # set transaction as JSON with all the attributes needed according to Swagger docs + req = s.post(self.db_adress + "/transaction", json=transaction, headers=headers) + return req.status_code + + + def get_user_portfolio(self, user_id, max_retries=10): + """gets the portfolio of the user + + Args: + user_id (int): id of the user + max_retries (int): max retries for the request + + Returns: + dict: dictionary of portfolio + """ + if max_retries <= 0: + return None + + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.get(self.db_adress + "/portfolio", headers=headers) # get portfolio as JSON + if req.status_code == 200: + portfolio_dict = req.json()["data"] # get the data of the JSON + return portfolio_dict + else: + return self.get_user_portfolio(user_id, max_retries-1) + + def set_cron_interval(self, user_id, cron_interval): + """sets the cron interval of the user + + Args: + user_id (int): id of the user + cron_interval (String): Update interval in cron format => see https://crontab.guru/ for formatting + + Returns: + int: status code + """ + if not croniter.is_valid(cron_interval): # check if cron_interval is in valid format + print("Error: Invalid cron format") + return -1 # return error code -1 if invalid cron format + + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} + req = s.put(self.db_adress + "/user/setCron", json={"cron": str(cron_interval)}, headers=headers) # put not post (see swagger docs) + return req.status_code + + + def set_admin(self, email, is_admin): + """sets the admin of the user + + Args: + email (string): email of the user + is_admin (bool): "true" if user should be Admin, "false" if not + + Returns: + int: status code + """ + with r.Session() as s: + headers = {'Authorization': 'Bearer ' + self.token} # only bot token is needed, user is chosen by email + req = s.put(self.db_adress + "/user/setAdmin", json={"admin": is_admin,"email": str(email)}, headers=headers) + return req.status_code + + +if __name__ == "__main__": # editable, just for basic on the go testing of new functions + + print("This is a module for the telegram bot. It is not intended to be run directly.") + handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env + print(handler.token) + keywords = handler.get_user_keywords(user_id = 1709356058) #user_id here is currently mine (Linus) + print(keywords) + shares = handler.get_user_portfolio(user_id = 1709356058) + print("set cron with status: "+ str(handler.set_cron_interval(user_id = 1709356058, cron_interval = "0 0 * * *"))) + user = handler.get_user(user_id = 1709356058) + print(user) + all_users = handler.get_all_users() + admin_status = handler.set_admin("test@test.com", "true") + print(admin_status) + print(all_users) + print(shares) + sys.exit(1) \ No newline at end of file diff --git a/telegram_bot/bot.py b/telegram_bot/bot.py index e3e4f28..346a610 100644 --- a/telegram_bot/bot.py +++ b/telegram_bot/bot.py @@ -3,8 +3,8 @@ script for telegram bot and its functions """ __author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "11.03.2022" -__version__ = "0.0.4" +__date__ = "26.04.2022" +__version__ = "1.2.2" __license__ = "None" # side-dependencies: none @@ -17,51 +17,31 @@ __license__ = "None" import os import telebot -import time import sys import logging -import json +import re import news.news_fetcher as news import shares.share_fetcher as share_fetcher +import datetime as dt from telebot import types from dotenv import load_dotenv +from api_handling.api_handler import API_Handler -load_dotenv() -bot_version = "0.2.1" -user_list = [] +load_dotenv(dotenv_path='.env') # load environment variables -class User: # Currently saving users in this class to test functionality -> later database - def __init__(self, p_user_id, p_user_name, p_chat_id): - - """ Initialize a new user - :type self: - :param self: for class - - :type p_user_id: int - :param p_user_id: telegram user id - - :type p_user_name: str - :param p_user_name: first name of user - - :type p_chat_id: int - :param p_chat_id: telegram chat id - - :raises: - - :rtype: - """ - - self.user_id = int(p_user_id) - self.chat_id = int(p_chat_id) - self.user_name = str(p_user_name) +bot_version = "1.0.1" # version of bot + +#create api handler +api_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env vars. +print("Webserver Token: " + str(api_handler.token)) bot = telebot.TeleBot(os.getenv('BOT_API_KEY')) -@bot.message_handler(commands=['start']) # /start -> saving as new user and sending welcome +@bot.message_handler(commands=['start', 'Start']) # /start -> saving as new user and sending welcome def send_start(message): """ Description @@ -72,18 +52,10 @@ def send_start(message): :rtype: none """ - new_user = User(int(message.from_user.id), message.from_user.first_name, int(message.chat.id)) - existing_already = False - for known_user in user_list: - if known_user.user_id == new_user.user_id: - existing_already = True - if existing_already == False: - user_list.append(new_user) - bot.reply_to(message, "Welcome to this share bot project. Type /help to get information on what this bot can do") -@bot.message_handler(commands=['version']) +@bot.message_handler(commands=['version', 'Version']) def send_version(message): """ Sending programm version @@ -94,11 +66,11 @@ def send_version(message): :rtype:none """ - bot.reply_to(message, bot_version) + bot.reply_to(message, "the current bot version is " + bot_version) -@bot.message_handler(commands=['help']) # /help -> sending all functions -def send_welcome(message): +@bot.message_handler(commands=['help', 'Help']) # /help -> sending all functions +def send_help(message): """ Send all functions :type message: message object bot @@ -108,10 +80,10 @@ def send_welcome(message): :rtype: none """ - bot.reply_to(message, "/id or /auth for authentication. /update to get updates on your shares. /users to see all users. /news to get current use for your keywords. /share to get price of specific share. For further details see aktienbot.flokaiser.com") + bot.reply_to(message, "/id or /auth get your user id\n/update get updates on your shares.\n/setAdmin set admin rights of user (ADMIN)\n/users see all users. (ADMIN)\n/me get my user info\n/news get top article for each keyword.\n/allnews get all news (last 7 days)\n/keywords get all your keywords\n/addkeyword add a keyword\n/removekeyword remove a keyword\n/share get price of specific share\n/portfolio see own portfolio\n/newtransaction add new transaction\n/interval get update interval\n/setinterval set update interval\n_For further details see https://gruppe1.testsites.info _", parse_mode='MARKDOWN') -@bot.message_handler(commands=['users']) +@bot.message_handler(commands=['users', 'Users']) # /users -> sending all users def send_all_users(message): """ Send all users, only possible for admins @@ -122,19 +94,97 @@ def send_all_users(message): :rtype: none """ - print('Debug: users command') - user_id = int(message.from_user.id) + + user_id = int(message.from_user.id) + user_data = api_handler.get_user(user_id) + if(user_data["admin"] == False): # check if user has admin rights + bot.reply_to(message, "You have to be an admin to use this command") + return - # tbd check if user is admin + user_list = api_handler.get_all_users() + user_count = len(user_list) + bot.send_message(chat_id=user_id, text="There are " + str(user_count) + " users in the database:") - answer = 'Current number of users: ' + str(len(user_list)) - bot.send_message(chat_id = user_id, text=answer) - for known_user in user_list: - answer = str(known_user.user_id) + ' : ' + known_user.user_name - bot.send_message(chat_id=user_id, text=answer) + for user in user_list: + + username = user['username'] + email = user['email'] + id = user['telegram_user_id'] + cron = user['cron'] + admin = user['admin'] + + bot.send_message(chat_id=user_id, text=f'Username: {username}\nEmail: {email}\nID: {id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text + +@bot.message_handler(commands=['setAdmin', 'SetAdmin', 'setadmin', 'Setadmin']) # set admin rights to user TBD: not working!! +def set_admin(message): -@bot.message_handler(commands=['id', 'auth']) # /auth or /id -> Authentication with user_id over web tool + """ Set admin rights to user + :type message: message object bot + :param message: message that was reacted to, in this case always containing '/setAdmin' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + user_data = api_handler.get_user(user_id) + + if(user_data["admin"] == False): # check if user has admin rights + bot.reply_to(message, "You have to be an admin to use this command") + return + + bot.send_message(chat_id=user_id, text='send email and true if this account should have admin rights, else false\n in format: ,') # request email and admin rights to change to + bot.register_next_step_handler(message, set_admin_step) + +def set_admin_step(message): + str_message = str(message.text) + args_message = str_message.split(',') # split message into email and admin rights + + if len(args_message) != 2: # make sure 2 args (email,is_admin) are given + + bot.reply_to(message, "exactly 2 arguments (,) required, try again") + return + + email = args_message[0] + is_admin = False # default: False + + if args_message[1].lower() == "true": # if user types true, set is_admin to true + is_admin = True + + status = api_handler.set_admin(email, is_admin) # set admin in db + + if(status == 200): + bot.reply_to(message, "Admin rights set") + + else: + bot.reply_to(message, f"Admin rights could not be set ({status})") + + +@bot.message_handler(commands=['me', 'Me']) # /me -> sending user info +def send_user(message): + """ Send user data + :type message: message object bot + :param message: message that was reacted to, in this case always containing '/me' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + user_data = api_handler.get_user(user_id) + if not user_data or user_data == None: # true if user is not registered + bot.reply_to(message, "This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info") + return + username = user_data['username'] + email = user_data['email'] + user_id = user_data['telegram_user_id'] + cron = user_data['cron'] + admin = user_data['admin'] + bot.reply_to(message, f'Username: {username}\nEmail: {email}\nID: {user_id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text + + +@bot.message_handler(commands=['id', 'auth', 'Id', 'Auth']) # /auth or /id -> Authentication with user_id over web tool def send_id(message): """ Send user id for authentication with browser @@ -145,49 +195,72 @@ def send_id(message): :rtype: none """ - answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on aktienbot.flokaiser.com to get updates on your shares.' + answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on https://gruppe1.testsites.info to get updates on your shares.' bot.reply_to(message, answer) -@bot.message_handler(commands=['update']) -def send_update(message): - - """ Send update on shares +#function that can be used to ensure that the bot is online and running +@bot.message_handler(commands=['status', 'Status']) +def send_status(message): + + """ Sends status to user :type message: message object bot - :param message: message that was reacted to, in this case always containing '/help' + :param message: message that was reacted to, if no other command handler gets called :raises: none :rtype: none """ - user_id = int(message.from_user.id) + bot.reply_to(message, "bot is running") + + +@bot.message_handler(commands=['update', 'Update']) # /update -> update shares +def update_for_user(message): - #Can be deleted when getting from database - dirname = os.path.dirname(__file__) - json_path = os.path.join(dirname, 'shares/shares_example.json') + p_user_id = int(message.from_user.id) + p_my_handler = api_handler - with open(json_path) as json_file: - json_share_data = json.load(json_file) - int_share_count = int(json_share_data['share_count']) - str_username = str(json_share_data['user']) - bot.send_message(chat_id=user_id, text=f'Hello {str_username}. Here is the update on your currently owned shares:') - - - for i in range(int_share_count): - - my_share = json_share_data['shares'][i] - my_share_symbol = str(my_share['symbol']) - my_share_amount = float(my_share['amount_bought']) - my_share_buy_price = float(my_share['price_bought']) - my_share_course = float(share_fetcher.get_share_price(my_share_symbol)) - - - my_update_message = f'Symbol: {my_share_symbol}\nPrice: {my_share_course}\nBought for: {my_share_buy_price}\n\ -Amount owned: {my_share_amount}\nWin/Lose: {(my_share_amount*my_share_course) - (my_share_amount*my_share_buy_price)}' - bot.send_message(chat_id=user_id, text=my_update_message) + share_symbols = [] + share_amounts = [] + share_courses = [] + + my_portfolio = p_my_handler.get_user_portfolio(p_user_id) + + for element in my_portfolio: + if element["count"] != '' and element["isin"]!= '': + print(element["count"], element["isin"]) + share_symbols.append(element["isin"]) + share_amounts.append(element["count"]) + share_courses.append(element["current_price"]) + + my_user = p_my_handler.get_user(p_user_id) + send_to_user("Hello %s this is your share update:"%str(my_user["username"]), pUser_id=p_user_id) + + if len(share_symbols) != 0: + for i in range(len(share_symbols)): + my_update_message = f'Symbol: {share_symbols[i]}\nCurrent Price per Share: {share_courses[i]}\nAmount owned: {share_amounts[i]}\nTotal Investment: {float(share_courses[i]) * float(share_amounts[i])}' + send_to_user(my_update_message, pUser_id=p_user_id) + else: + send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id) + + +def send_to_user(pText, pUser_id): + + """ Send message to user + :type pText: string + :param pText: Text to send to user + + :type pUser_id: int + :param pUser_id: user to send to. per default me (Florian Kellermann) + + :raises: none + + :rtype: none + """ + bot.send_message(chat_id=pUser_id, text=pText) -@bot.message_handler(commands=['share']) +@bot.message_handler(commands=['share', 'Share']) # /share -> get share price def send_share_update(message): """ Send price of a specific share @@ -207,13 +280,50 @@ def send_share_update(message): def send_share_price(message): str_share_price = share_fetcher.get_share_price(str(message.text)) - bot.reply_to(message, str_share_price) + bot.reply_to(message, str_share_price) # add dollar symbol etc. -@bot.message_handler(commands=['news']) -def send_news(message): +@bot.message_handler(commands=['allnews', 'Allnews']) # /allnews -> get all news +def send_all_news(message): """ Get news for keywords of user + :type message: message object bot + :param message: message that was reacted to, in this case always containing '/allnews' + + :raises: none + + :rtype: none + """ + + user_id = int(message.from_user.id) + keywords = api_handler.get_user_keywords(user_id) # get keywords of user + + if keywords == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') + return + + if not keywords: # true if user is registered but does not have any keywords + bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /news') + return + + keywords_search = ' OR '.join(keywords) # concat all keywords with OR -> NewsAPI can understand OR, AND, NOT etc. + now = dt.datetime.now().date() # get current date + from_date = now - dt.timedelta(days=7) # get date 7 days ago -> limit age of news to 7 days old max + from_date_formatted = dt.datetime.strftime(from_date, '%Y-%m-%d') + news_list = news.get_all_news_by_keyword(keywords_search, from_date_formatted)["articles"] # array of JSON article objects + + if news_list: # true if news_list is not empty + for article in news_list: + formatted_article = news.format_article(article) + bot.send_message(chat_id=user_id, text=formatted_article, parse_mode="MARKDOWN") # Markdown allows to write bold text with * etc. + else: + bot.send_message(chat_id=user_id, text='No news found for your keywords.') + + +@bot.message_handler(commands=['news', 'News']) # /news -> get news for specific keyword +def send_news(message): + """ Get news for keywords of user + :type message: message object bot :param message: message that was reacted to, in this case always containing '/news' @@ -221,21 +331,222 @@ def send_news(message): :rtype: none """ - - keyword = "bitcoin" user_id = int(message.from_user.id) - #Get Information for user with this id + keywords = api_handler.get_user_keywords(user_id) # get keywords of user - articles = news.get_top_news_by_keyword(keyword) #tbd: get keyword from db - try: - formatted_article = news.format_article(articles["articles"][0]) - except IndexError: - bot.send_message(chat_id=user_id, text=f"no news currently available for keyword: {keyword}") + if keywords == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') return - bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWN") + + if not keywords: # true if user is registered but does not have any keywords + bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword') + return + + if keywords: + for keyword in keywords: + top_news = news.get_top_news_by_keyword(keyword)["articles"] + if top_news == None: # true if request to NewsAPI failed + bot.send_message(chat_id=user_id, text='News Server did not respond correctly. Try again later.') + + if not top_news: # true if no news found for keyword (empty list) + bot.send_message(chat_id=user_id, text=f'No news found for keyword: *{keyword}*', parse_mode="MARKDOWN") + + else: + formatted_article = news.format_article(top_news[0]) # only format and send most popular news + bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWN") -@bot.message_handler(func=lambda message: True) # Returning that command is unkown for any other statement +@bot.message_handler(commands=['addkeyword', 'Addkeyword']) # /addkeyword -> add keyword to user +def add_keyword(message): + """ Add keyword to user + :type message: message object bot + :param message: message that was reacted to, in this case always '/addkeyword' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + bot.send_message(chat_id=user_id, text='Type keyword to add:') + bot.register_next_step_handler(message, store_keyword) # wait for user to send keyword, then call store_keyword function + +def store_keyword(message): + user_id = int(message.from_user.id) + keyword = str(message.text).lower() # lower to ensure Bitcoin and bitcoin is not stored as individual keywords + status = api_handler.set_keyword(user_id, keyword) # set keyword in database + if status == 200: # statuscode 200 means keyword was added successfully without errors + bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" added.') # duplicate keywords are denied by Database, so no need to check for that here + else: + bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" could not be stored. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info (statuscode {status})') + + +@bot.message_handler(commands=['removekeyword', 'Removekeyword']) # /removekeyword -> remove keyword from user +def remove_keyword(message): + """ Remove keyword from user + :type message: message object bot + :param message: message that was reacted to, in this case always '/removekeyword' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + bot.send_message(chat_id=user_id, text='Type keyword to remove:') + bot.register_next_step_handler(message, remove_keyword_step) # wait for user to send keyword to remove, then call remove_keyword_step function + +def remove_keyword_step(message): + user_id = int(message.from_user.id) + keyword = str(message.text).lower() + status = api_handler.delete_keyword(user_id, keyword) + if status == 200: # statuscode 200 means keyword was removed successfully without errors + bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" removed.') # checking if keyword to remove is in database are handled in database, not here + else: + bot.send_message(chat_id=user_id, text=f'Failed deleting keyword "{keyword}". (statuscode {status})') + + +@bot.message_handler(commands=['keywords', 'Keywords']) # /keywords -> get keywords of user +def send_keywords(message): + """ Send keywords of user + :type message: message object bot + :param message: message that was reacted to, in this case always '/keywords' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + keywords = api_handler.get_user_keywords(user_id) # get keywords of user + if keywords == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') + return + if not keywords: # true if user is registered but does not have any keywords + bot.send_message(chat_id=user_id, text='No keywords set for this account. Add keywords by using /addkeyword') + return + else: # send keyword list + keywords_str = ', '.join(keywords) + bot.send_message(chat_id=user_id, text=f'Your keywords are: _{keywords_str}_', parse_mode="MARKDOWN") + + +@bot.message_handler(commands=['portfolio', 'Portfolio']) +def send_portfolio(message): + """ Send portfolio of user + :type message: message object bot + :param message: message that was reacted to, in this case always '/portfolio' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + portfolio = api_handler.get_user_portfolio(user_id) # get portfolio of user as json + if portfolio == None: # true if user is not registered + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info') + return + if not portfolio: # true if user is registered but does not have any stocks in portfolio + bot.send_message(chat_id=user_id, text='You do not have any stocks in your portfolio.') + return + else: # send portfolio + for stock in portfolio: + comment = str(stock["comment"]) # comment may be written name of stock, comment is made by user when adding an stock to portfolio + count = "{:.2f}".format(float(stock["count"])) # round count to 2 decimal places + isin = str(stock["isin"]) + worth = "{:.2f}".format(float(stock["current_price"]) * float(stock["count"])) # round current_price to 2 decimal places + bot.send_message(chat_id=user_id, text=f'*{comment}*\n_{isin}_\namount: {count}\nworth: ${worth}', parse_mode="MARKDOWN") # formatted message in markdown + + +@bot.message_handler(commands=['newtransaction', 'Newtransaction']) #tbd not working rn may be deleted in future +def set_new_transaction(message): + """ Set new transaction for user + :type message: message object bot + :param message: message that was reacted to, in this case always '/newtransaction' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + bot.send_message(chat_id=user_id, text='Type ",,," (time of transaction will be set to now):') + bot.register_next_step_handler(message, set_new_transaction_step) + +def set_new_transaction_step(message): + user_id = int(message.from_user.id) + + if not re.match(r"[A-Za-z0-9]+,[A-Za-z0-9]+,[0-9]+(.[0-9]+)?,[0-9]+(.[0-9]+)?", message.text): + bot.send_message(chat_id=user_id, text='Invalid format \n(e.g. Apple,US0378331005,53.2,120.4).\n Try again with /newtransaction.') + return + + transaction_data = str(message.text).split(',') + desc = str(transaction_data[0]) + isin = str(transaction_data[1]) + amount = float(transaction_data[2]) + price = float(transaction_data[3]) + time = dt.datetime.now().isoformat() + print("\n\n\n\n\n") + print(f"{isin},{amount},{price},{time}") + status = api_handler.set_transaction(user_id, desc, isin, amount, price, time) + + if status == 200: + bot.send_message(chat_id=user_id, text='Transaction succesfully added.') + else: + bot.send_message(chat_id=user_id, text=f'Failed adding transaction. (statuscode {status})') + + +@bot.message_handler(commands=['interval', 'Interval']) +def send_interval(message): + """ send interval for user + :type message: message object bot + :param message: message that was reacted to, in this case always '/interval' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + user_data = api_handler.get_user(user_id) # get cron interval of user (stored in user data) + if user_data == None: # true if user is not registered in DB + bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info and set an interval with /setinterval') + return + else: # send interval + interval = str(user_data['cron']) # get cron from user data + if interval == 'None': # true if user has no cron set + bot.send_message(chat_id=user_id, text='You do not have an interval set. Set one with /setinterval') + return + formatted_interval = str(interval).replace(' ', '_') # replace spaces with underscores to add to url of crontab.guru + bot.send_message(chat_id=user_id, text=f'Your update interval: {interval} (https://crontab.guru/#{formatted_interval})') + + +@bot.message_handler(commands=['setinterval', 'Setinterval']) +def set_new_interval(message): + """ Set new interval for user + :type message: message object bot + :param message: message that was reacted to, in this case always '/setinterval' + + :raises: none + + :rtype: none + """ + user_id = int(message.from_user.id) + bot.send_message(chat_id=user_id, text='Type interval in cron format:\n(https://crontab.guru/)') + bot.register_next_step_handler(message, set_new_interval_step) # executes function when user sends message + +def set_new_interval_step(message): + + user_id = int(message.from_user.id) + interval = str(message.text) + status = api_handler.set_cron_interval(user_id, interval) # send cron to db + + if status == 200: + bot.send_message(chat_id=user_id, text='Interval succesfully set.') + return + + if status == -1: # only -1 when interval is invalid, not a real statuscode, but used from api_handler.set_cron_interval to tell the crontab has the wrong format + bot.send_message(chat_id=user_id, text='Invalid interval format. Try again with\n /setinterval.') + return + else: + bot.send_message(chat_id=user_id, text=f'Failed setting interval. (statuscode {status})') + + +@bot.message_handler(func=lambda message: True) # Returning that command is unknown for any other statement def echo_all(message): """ Tell that command is not known if it is no known command @@ -274,15 +585,12 @@ def query_text(inline_query): def main_loop(): - """ Get Information about bot status every 3 seconds + """ Start bot :raises: none :rtype: none """ bot.infinity_polling() - while 1: - time.sleep(3) - if __name__ == '__main__': try: diff --git a/telegram_bot/bot_updates.py b/telegram_bot/bot_updates.py new file mode 100644 index 0000000..f48a824 --- /dev/null +++ b/telegram_bot/bot_updates.py @@ -0,0 +1,193 @@ +""" +script for regularly sending updates on shares and news based on user interval +""" +__author__ = "Florian Kellermann, Linus Eickhoff" +__date__ = "26.04.2022" +__version__ = "1.0.2" +__license__ = "None" + +from dotenv import load_dotenv +import news.news_fetcher as news_fetcher +import time +import os +from bot import bot +import sys +from apscheduler.schedulers.background import BackgroundScheduler +from api_handling.api_handler import API_Handler + + +''' +* * * * * code +┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ +│ │ │ │ └──── weekday (0->Monday, 7->Sunday) +│ │ │ └────── Month (1-12) +│ │ └──────── Day (1-31) +│ └────────── Hour (0-23) +└──────────── Minute (0-59) + +example 0 8 * * * -> daily update at 8am +''' +user_ids = [] +user_crontab = [] + +load_dotenv(dotenv_path='.env') + +def start_updater(): + """ starting function for regularly sending updates + :raises: none + + :rtype: none + """ + + print("Bot updates started") + + my_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) + + + update_crontab(my_handler) + + +def update_crontab(p_my_handler): + """ Updating crontab lists every hour + :type pCurrent_Time: time when starting crontab update + :param pCurrent_Time: datetime + + :raises: none + + :rtype: none + """ + + global user_crontab + global user_ids + + all_users = p_my_handler.get_all_users() + + user_ids = [] + user_crontab = [] + + for element in all_users: + if element["cron"] != '' and element["telegram_user_id"] != '': + user_ids.append(int(element["telegram_user_id"])) + user_crontab.append(str(element["cron"])) + + print(user_ids) + + update_based_on_crontab(user_ids, user_crontab, p_my_handler) + + update_crontab(p_my_handler) + + +def update_based_on_crontab(p_user_ids, p_user_crontab, p_my_handler): + + """ Check all the crontab codes and add jobs to start in time + :type p_user_ids: array + :param p_user_ids: user id array of all users + + :type p_user_crontab: array + :param p_user_crontab: crontabs for all users equivalent to the user array + + :type p_my_handler: Api_Handler + :param p_my_handler: get database stuff + + :raises: none + + :rtype: none + """ + + my_scheduler = BackgroundScheduler() + + for i in range(len(p_user_ids)): + cron_split = p_user_crontab[i].split(" ") + print(cron_split[4], cron_split[1], cron_split[0], cron_split[3], cron_split[2]) + my_scheduler.add_job(update_for_user, 'cron', day_of_week = cron_split[4] , hour= cron_split[1] , minute = cron_split[0], month= cron_split[3] , day=cron_split[2], args=(p_user_ids[i], p_my_handler )) + + my_scheduler.start() + + time.sleep( 600 ) + my_scheduler.shutdown() + +def update_for_user(p_user_id, p_my_handler): + + """ Pull shares and send updates for specific user id + :type p_user_id: integer + :param p_user_id: user id of user that shall receive update + + :type p_my_handler: Api_Handler + :param p_my_handler: handle the api and pull from database + + :raises: none + + :rtype: none + """ + share_symbols = [] + share_amounts = [] + share_courses = [] + + my_portfolio = p_my_handler.get_user_portfolio(p_user_id) + + for element in my_portfolio: + if element["count"] != '' and element["isin"]!= '': + print(element["count"], element["isin"]) + share_symbols.append(element["isin"]) + share_amounts.append(element["count"]) + share_courses.append(element["current_price"]) + + my_user = p_my_handler.get_user(p_user_id) + send_to_user("Hello %s this is your share update for today:"%str(my_user["username"]), pUser_id=p_user_id) + + if len(share_symbols) != 0: + for i in range(len(share_symbols)): + my_update_message = f'Symbol: {share_symbols[i]}\nCurrent Price per Share: {share_courses[i]}\nAmount owned: {share_amounts[i]}\nTotal Investment: {float(share_courses[i]) * float(share_amounts[i])}' + send_to_user(my_update_message, pUser_id=p_user_id) + else: + send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id) + + keywords = p_my_handler.get_user_keywords(p_user_id) # get keywords as array + + if(keywords): # if keywords exist and array is not empty + send_to_user("If you haven't read yet: \nHere are some interesting news according to your keywords:", pUser_id=p_user_id) + for keyword in keywords: + news = news_fetcher.get_top_news_by_keyword(keyword)["articles"] + + if not news: # if empty news array + send_to_user(f"No news found for keyword _{keyword}_.", pUser_id=p_user_id, md_mode=True) + + if news == None: # if news is none + send_to_user(f"Server error for keyword _{keyword}_.", pUser_id=p_user_id, md_mode=True) + else: + news_formatted = news_fetcher.format_article(news[0]) # format for message, only use the most popular article + send_to_user(f"_keyword: {keyword}_\n\n{news_formatted}", pUser_id=p_user_id, md_mode=True) # send news with related keyword in Markdown + + + +def send_to_user(pText, pUser_id , md_mode = False): + + """ Send message to user + :type pText: string + :param pText: Text to send to user + + :type pUser_id: int + :param pUser_id: user to send to. per default me (Florian Kellermann) + + :type md_mode: boolean + :param md_mode: if true, parse_mode is markdown + + :raises: none + + :rtype: none + """ + if md_mode: + bot.send_message(chat_id=pUser_id, text=pText, parse_mode="MARKDOWN") + else: + bot.send_message(chat_id=pUser_id, text=pText) + + + +if __name__ == "__main__": + try: + start_updater() + sys.exit(-1) + except KeyboardInterrupt: + print("Ending") + sys.exit(-1) \ No newline at end of file diff --git a/telegram_bot/db_handler.py b/telegram_bot/db_handler.py deleted file mode 100644 index 816af71..0000000 --- a/telegram_bot/db_handler.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -script for database interaction -""" -__author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "15.03.2022" -__version__ = "0.0.1" -__license__ = "None" - -# get db_key from env - -class DB_Handler: - - def __init__(self, db_adress): - # tbd - return \ No newline at end of file diff --git a/telegram_bot/deploy/healthcheck.sh b/telegram_bot/deploy/healthcheck.sh deleted file mode 100644 index 390f507..0000000 --- a/telegram_bot/deploy/healthcheck.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -curl -s http://localhost:80/ -o /dev/null || exit 1 diff --git a/telegram_bot/deploy/start.sh b/telegram_bot/deploy/start.sh index 4ee7078..75c6e21 100644 --- a/telegram_bot/deploy/start.sh +++ b/telegram_bot/deploy/start.sh @@ -1,2 +1,4 @@ #!/usr/bin/env sh -python bot.py \ No newline at end of file + +python bot.py & +python bot_updates.py \ No newline at end of file diff --git a/telegram_bot/news/news_fetcher.py b/telegram_bot/news/news_fetcher.py index 69c0807..bd083b5 100644 --- a/telegram_bot/news/news_fetcher.py +++ b/telegram_bot/news/news_fetcher.py @@ -2,28 +2,48 @@ script for news fetching (by keywords) """ __author__ = "Florian Kellermann, Linus Eickhoff" -__date__ = "15.03.2022" -__version__ = "0.0.1" +__date__ = "26.04.2022" +__version__ = "1.0.0" __license__ = "None" import sys import os -import json import requests from newsapi import NewsApiClient from dotenv import load_dotenv -load_dotenv() +load_dotenv() # loads environment vars # Init -api_key = os.getenv('NEWS_API_KEY') -newsapi = NewsApiClient(api_key=api_key) -source_json = requests.get(f"https://newsapi.org/v2/top-headlines/sources?apiKey={api_key}&language=en").json() -sources = source_json["sources"] -str_sources = ",".join([source["id"] for source in sources]) +api_key = os.getenv('NEWS_API_KEY') # get API Key from .env file +newsapi = NewsApiClient(api_key=api_key) # news api from https://newsapi.org/ +try: + # get all available news sources (e.g BBC, New York Times, etc.) + source_json = requests.get(f"https://newsapi.org/v2/top-headlines/sources?apiKey={api_key}&language=en").json() + sources = source_json["sources"] + str_sources = ",".join([source["id"] for source in sources]) +except KeyError: + print("Error: Could not get sources") + sys.exit(1) +def get_all_news_by_keyword(keyword, from_date="2000-01-01"): + """get all news to keyword + Args: + keyword (String): keyword for search + from_date (String): min date for search + + Returns: + JSON/dict: dict containing articles + """ + top_headlines = newsapi.get_everything(q=keyword, sources=str_sources, language='en', from_param=from_date) # keywords can be combined with OR (e.g. keyword = "bitcoin OR ethereum") + if(top_headlines["status"] == "ok"): + return top_headlines + else: + return None + + def get_top_news_by_keyword(keyword): """get top news to keyword Args: @@ -31,9 +51,13 @@ def get_top_news_by_keyword(keyword): Returns: JSON/dict: dict containing articles - """ - top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en') - return top_headlines + """ + top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en') # get top headlines, measured by popularity from NewsApi + if(top_headlines["status"] == "ok"): + return top_headlines + else: + return None + def format_article(article): """format article for messaging (using markdown syntax) @@ -47,14 +71,19 @@ def format_article(article): sourcename = article["source"]["name"] headline = article["title"] url = article["url"] - formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}" + formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}" # formatting in Markdown syntax return formatted_article -if __name__ == '__main__': +if __name__ == '__main__': # only execute if script is called directly -> for simple testing - print("fetching top news by keyword business...") + print("this is a module and should not be run directly") + print("fetching top news by keyword bitcoin...") + articles = get_all_news_by_keyword("bitcoin") + formatted_article = format_article(articles["articles"][0]) + print(formatted_article) articles = get_top_news_by_keyword("bitcoin") formatted_article = format_article(articles["articles"][0]) - print(formatted_article) \ No newline at end of file + print(formatted_article) + sys.exit(1) \ No newline at end of file diff --git a/telegram_bot/requirements.txt b/telegram_bot/requirements.txt index 4dcfe5c..fe33bb0 100644 --- a/telegram_bot/requirements.txt +++ b/telegram_bot/requirements.txt @@ -1,6 +1,9 @@ -pyTelegramBotAPI~=4.4.0 +pyTelegramBotAPI~=4.5.0 Markdown~=3.3.6 yfinance~=0.1.70 newsapi-python~=0.2.6 -python-dotenv~=0.19.2 +python-dotenv~=0.20.0 requests~=2.27.1 +APScheduler~=3.9.1 +croniter~=1.3.4 +tzlocal==2.1 diff --git a/telegram_bot/shares/share_fetcher.py b/telegram_bot/shares/share_fetcher.py index 1aa3376..c7ec63f 100644 --- a/telegram_bot/shares/share_fetcher.py +++ b/telegram_bot/shares/share_fetcher.py @@ -7,7 +7,6 @@ __version__ = "0.0.2" __license__ = "None" import yfinance -import json def get_share_price(str_symbol):