diff --git a/.woodpecker/pipeline.yml b/.woodpecker/pipeline.yml index d202d96..7a0d6a1 100644 --- a/.woodpecker/pipeline.yml +++ b/.woodpecker/pipeline.yml @@ -4,6 +4,7 @@ pipeline: commands: - echo -n "${CI_COMMIT_BRANCH//\//-}-${CI_COMMIT_SHA:0:8}, latest" > .tags when: + path: [ "frontend/**", "telegram_bot/**", "api/**" ] event: push @@ -11,7 +12,7 @@ pipeline: build_api: image: woodpeckerci/plugin-docker-buildx settings: - repo: + repo: from_secret: repo_api username: from_secret: username @@ -30,7 +31,7 @@ pipeline: build_bot: image: woodpeckerci/plugin-docker-buildx settings: - repo: + repo: from_secret: repo_bot username: from_secret: username @@ -49,7 +50,7 @@ pipeline: build_frontend: image: woodpeckerci/plugin-docker-buildx settings: - repo: + repo: from_secret: repo_frontend username: from_secret: username @@ -79,7 +80,8 @@ pipeline: - cd /root/docker/aktienbot - docker-compose pull - docker-compose -p "aktienbot" up -d - when: - event: push - + when: + path: [ "frontend/**", "telegram_bot/**", "api/**" ] + event: push + branches: main diff --git a/README.md b/README.md index 022b2bc..0bb8abd 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,21 @@ WebEngineering2 Projekt: Aktien und News Bot für Telegram ## Dokumentation -> README.md in /documentation + +## 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 index 25dc0bf..2d183f9 100644 --- a/api/.env.example +++ b/api/.env.example @@ -16,3 +16,6 @@ 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 cfc57aa..c9a163b 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,16 +1,16 @@ -FROM python:3.10-alpine +FROM python:3.10-slim -# Change the working directory to the project root +# Change the working directory to the root of the project WORKDIR /srv/flask_app # Install dependencies -RUN apk add nginx build-base libffi-dev curl uwsgi +RUN apt update && apt install -y python3 python3-pip curl nginx && rm -rf /var/lib/apt/lists/* -# Install python dependencies +# 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 app +# Copy the source code to the working directory COPY api /srv/flask_app COPY api/deploy/nginx.conf /etc/nginx diff --git a/api/app.py b/api/app.py index 3314930..28fe3e9 100644 --- a/api/app.py +++ b/api/app.py @@ -1,3 +1,9 @@ +__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 app import create_app # Create an application instance that web servers can use. diff --git a/api/app/__init__.py b/api/app/__init__.py index c62e70c..30822d3 100644 --- a/api/app/__init__.py +++ b/api/app/__init__.py @@ -1,3 +1,9 @@ +__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 @@ -7,6 +13,7 @@ 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 @@ -23,13 +30,12 @@ def create_app(config_filename=None): CORS(application, resources={r"*": {"origins": "*"}}) - application.app_context().push() - 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) diff --git a/api/app/auth.py b/api/app/auth.py index 513a889..e96d28a 100644 --- a/api/app/auth.py +++ b/api/app/auth.py @@ -1,3 +1,9 @@ +__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 diff --git a/api/app/blueprints/__init__.py b/api/app/blueprints/__init__.py index e69de29..dfaac6b 100644 --- a/api/app/blueprints/__init__.py +++ 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/app/blueprints/keyword.py b/api/app/blueprints/keyword.py index cd00c53..4412621 100644 --- a/api/app/blueprints/keyword.py +++ b/api/app/blueprints/keyword.py @@ -1,3 +1,9 @@ +__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 diff --git a/api/app/blueprints/portfolio.py b/api/app/blueprints/portfolio.py index 465ce47..2b6ab02 100644 --- a/api/app/blueprints/portfolio.py +++ b/api/app/blueprints/portfolio.py @@ -1,11 +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 os from apiflask import APIBlueprint - -from app.schema import PortfolioResponseSchema +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.auth import auth +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__))) @@ -23,11 +29,18 @@ def get_portfolio(): if transactions is not None: for row in transactions: - return_portfolio.append({ + data = { "symbol": row[0], "count": row[1], - # "price": row[2], - "last_transaction": row[3] - }) + # "calculated_price": row[2], + "last_transaction": row[3], + 'current_price': 0 + } + + query_share_price = db.session.query(SharePrice).filter_by(symbol=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..aa2a302 --- /dev/null +++ b/api/app/blueprints/share_price.py @@ -0,0 +1,94 @@ +__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 + +from apiflask import APIBlueprint, abort + +from app.models import SharePrice +from app.db import database as db +from app.helper_functions import make_response +from app.auth import auth +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(): + symbols = db.session.execute("SELECT symbol FROM `transactions` GROUP BY symbol;").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="Returns all transaction symbols", description="Returns all transaction symbols for all users") +def add_symbol_price(data): + if not check_if_symbol_data_exists(data): + abort(400, message="Symbol 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") + + symbol = data['symbol'] + price = data['price'] + time = data['time'] + + share_price = SharePrice( + symbol=symbol, + price=price, + date=datetime.datetime.strptime(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_symbol_data_exists(data): + if 'symbol' not in data: + return False + + if data['symbol'] == "" or data['symbol'] 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 index 24656a1..56dad69 100644 --- a/api/app/blueprints/shares.py +++ b/api/app/blueprints/shares.py @@ -1,3 +1,9 @@ +__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 diff --git a/api/app/blueprints/telegram.py b/api/app/blueprints/telegram.py index 5208d71..10aad9f 100644 --- a/api/app/blueprints/telegram.py +++ b/api/app/blueprints/telegram.py @@ -1,12 +1,16 @@ +__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.db import database as db -from app.helper_functions import make_response, get_email_or_abort_401 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 -from app.models import User telegram_blueprint = APIBlueprint('telegram', __name__, url_prefix='/api') __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) @@ -23,7 +27,8 @@ def add_keyword(data): if not check_if_telegram_user_id_data_exists(data): abort(400, message="User ID missing") - query_user = db.session.query(User).filter_by(email=email).first() + query_user = get_user(email) + query_user.telegram_user_id = data['telegram_user_id'] db.session.commit() diff --git a/api/app/blueprints/transactions.py b/api/app/blueprints/transactions.py index 33eae41..da3b7bf 100644 --- a/api/app/blueprints/transactions.py +++ b/api/app/blueprints/transactions.py @@ -1,13 +1,19 @@ -import os +__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 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 -from app.auth import auth transaction_blueprint = APIBlueprint('transaction', __name__, url_prefix='/api') __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) diff --git a/api/app/blueprints/user.py b/api/app/blueprints/user.py index 771bd60..196af26 100644 --- a/api/app/blueprints/user.py +++ b/api/app/blueprints/user.py @@ -1,15 +1,20 @@ +__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 -from flask import current_app import jwt from apiflask import APIBlueprint, abort - -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 -from app.models import User -from app.schema import UsersSchema, TokenSchema, LoginDataSchema, AdminDataSchema, DeleteUserSchema, RegisterDataSchema, UpdateUserDataSchema 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__))) @@ -36,9 +41,9 @@ def users(): def user(): email = get_email_or_abort_401() - res = db.session.query(User).filter_by(email=email).first().as_dict() + query_user = get_user(email) - return make_response(res, 200, "Successfully received current user data") + return make_response(query_user.as_dict(), 200, "Successfully received current user data") @users_blueprint.route('/user/login', methods=['POST']) @@ -55,10 +60,7 @@ def login(data): email = data['email'] password = data['password'] - query_user = db.session.query(User).filter_by(email=email).first() - - if query_user is None: # email doesn't exist - abort(500, message="Unable to login") + query_user = get_user(email) if not check_password(query_user.password, password.encode("utf-8")): # Password incorrect abort(500, message="Unable to login") @@ -98,7 +100,8 @@ def register(data): email=email, username=username, password=hash_password(password), - admin=False + admin=False, + cron="0 8 * * *" ) db.session.add(new_user) db.session.commit() @@ -114,7 +117,7 @@ def register(data): def update_user(data): email = get_email_or_abort_401() - query_user = db.session.query(User).filter_by(email=email).first() + query_user = get_user(email) if check_if_password_data_exists(data): query_user.password = hash_password(data['password']) @@ -144,10 +147,7 @@ def set_admin(data): email = data['email'] admin = data['admin'] - query_user = db.session.query(User).filter_by(email=email).first() - - if query_user is None: # Username doesn't exist - abort(500, message="Unable to update user") + query_user = get_user(email) query_user.admin = admin db.session.commit() @@ -155,6 +155,23 @@ def set_admin(data): 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() + + if not check_if_cron_data_exists(data): + abort(400, "Cron data missing") + + 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) @@ -216,3 +233,13 @@ def check_if_admin_data_exists(data): 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 index 25babd4..2fec222 100644 --- a/api/app/config/flask.cfg +++ b/api/app/config/flask.cfg @@ -1,3 +1,9 @@ +__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 diff --git a/api/app/config/flask_test.cfg b/api/app/config/flask_test.cfg index 2cc085b..fc8ac7f 100644 --- a/api/app/config/flask_test.cfg +++ b/api/app/config/flask_test.cfg @@ -1,3 +1,9 @@ +__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 diff --git a/api/app/db.py b/api/app/db.py index d09ee30..22b1d85 100644 --- a/api/app/db.py +++ b/api/app/db.py @@ -1,3 +1,9 @@ +__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 diff --git a/api/app/helper_functions.py b/api/app/helper_functions.py index 516d0e7..d3edea3 100644 --- a/api/app/helper_functions.py +++ b/api/app/helper_functions.py @@ -1,12 +1,16 @@ -from flask import current_app +__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 flask import request, jsonify - 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): @@ -17,45 +21,47 @@ def check_password(hashed_password, user_password): return bcrypt.checkpw(user_password, hashed_password) -def get_email_from_token_data(): - if 'Authorization' in request.headers: - token = request.headers['Authorization'].split(" ") +def get_email_from_token_data(token): + if token is None or len(token) < 2: + return None + else: + token = token[1] - if len(token) < 2: - return None - else: - token = token[1] + if token is not None: + if ':' in token: # Maybe bot token, check if token valid and return username after ":" then + telegram_user_id = token.split(":")[1] + token = token.split(":")[0] - if token is not None: - if ':' in token: # Maybe bot token, check if token valid and return username after ":" then - telegram_user_id = token.split(":")[1] - token = token.split(":")[0] + try: + 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() - try: - 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() - - if res is not None: - return res.as_dict()['email'] - else: - return None + if res is not None: + return res.as_dict()['email'] else: return None - except jwt.PyJWTError: + else: return None + except jwt.PyJWTError: + return None - else: # "Normal" token, extract username from token - try: - return jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])['email'] - except jwt.PyJWTError: - return None + else: # "Normal" token, extract username from token + try: + return jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])['email'] + except jwt.PyJWTError: + return None - return None + +def get_token(): + if 'Authorization' in request.headers: + return request.headers['Authorization'].split(" ") + else: + return None def get_email_or_abort_401(): # get username from jwt token - email = get_email_from_token_data() + 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") @@ -76,3 +82,12 @@ def is_user_admin(): def make_response(data, status=200, text=""): return jsonify({"status": status, "text": text, "data": data}) + + +def get_user(email): + query_user = db.session.query(User).filter_by(email=email).first() + + if query_user is None: # Username doesn't exist + abort(500, message="Can't find user") + + return query_user diff --git a/api/app/models.py b/api/app/models.py index 6c06c7b..864ce32 100644 --- a/api/app/models.py +++ b/api/app/models.py @@ -1,3 +1,9 @@ +__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 app.db import database as db @@ -7,14 +13,16 @@ class User(db.Model): 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') + 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 + "admin": self.admin, + "cron": self.cron } @@ -49,3 +57,14 @@ class Share(db.Model): 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) + symbol = db.Column('symbol', 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 index f425825..f48f232 100644 --- a/api/app/schema.py +++ b/api/app/schema.py @@ -1,3 +1,9 @@ +__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 apiflask import Schema from apiflask.fields import Integer, String, Boolean, Field, Float from marshmallow import validate @@ -23,6 +29,10 @@ class AdminDataSchema(Schema): admin = Boolean() +class CronDataSchema(Schema): + cron = String() + + class TokenSchema(Schema): token = String() @@ -71,6 +81,12 @@ class TransactionSchema(Schema): price = Float() +class SymbolPriceSchema(Schema): + symbol = 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() diff --git a/api/generate_sample_transactions.py b/api/generate_sample_transactions.py new file mode 100644 index 0000000..3c28db4 --- /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, 1000): + 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/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/requirements.txt b/api/requirements.txt index 2803098..0824c0d 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -10,4 +10,7 @@ flask-cors==3.0.10 bcrypt==3.2.0 pytest~=7.1.1 pytest-cov -marshmallow~=3.15.0 \ No newline at end of file +marshmallow~=3.15.0 +faker~=13.3.4 +yfinance~=0.1.70 +requests~=2.27.1 \ No newline at end of file diff --git a/api/tests/conftest.py b/api/tests/conftest.py index e1f32fc..be4933c 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -1,3 +1,9 @@ +__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 diff --git a/api/tests/functional/__init__.py b/api/tests/functional/__init__.py index e69de29..dfaac6b 100644 --- a/api/tests/functional/__init__.py +++ 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 index 9e68226..8c93f71 100644 --- a/api/tests/functional/helper_functions.py +++ b/api/tests/functional/helper_functions.py @@ -1,3 +1,9 @@ +__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 @@ -8,4 +14,4 @@ def get_token(test_client, email, password): if "token" in json.loads(response.data)["data"]: return json.loads(response.data)["data"]["token"] - return "" \ No newline at end of file + return "" diff --git a/api/tests/functional/test_keyword.py b/api/tests/functional/test_keyword.py index 3517c18..3ce610d 100644 --- a/api/tests/functional/test_keyword.py +++ b/api/tests/functional/test_keyword.py @@ -1,3 +1,9 @@ +__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. """ diff --git a/api/tests/functional/test_portfolio.py b/api/tests/functional/test_portfolio.py index f240c93..b2278ba 100644 --- a/api/tests/functional/test_portfolio.py +++ b/api/tests/functional/test_portfolio.py @@ -1,3 +1,9 @@ +__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. """ diff --git a/api/tests/functional/test_share.py b/api/tests/functional/test_share.py index 8df2ced..875bc0b 100644 --- a/api/tests/functional/test_share.py +++ b/api/tests/functional/test_share.py @@ -1,3 +1,9 @@ +__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. """ diff --git a/api/tests/functional/test_telegram.py b/api/tests/functional/test_telegram.py index 34acee7..17679df 100644 --- a/api/tests/functional/test_telegram.py +++ b/api/tests/functional/test_telegram.py @@ -1,3 +1,9 @@ +__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. """ diff --git a/api/tests/functional/test_transaction.py b/api/tests/functional/test_transaction.py index cb7b9ad..2f1917d 100644 --- a/api/tests/functional/test_transaction.py +++ b/api/tests/functional/test_transaction.py @@ -1,3 +1,9 @@ +__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. """ diff --git a/api/tests/functional/test_user.py b/api/tests/functional/test_user.py index 8b57338..8b4c1eb 100644 --- a/api/tests/functional/test_user.py +++ b/api/tests/functional/test_user.py @@ -1,7 +1,14 @@ +__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 @@ -35,7 +42,7 @@ def test_login_user_not_exist(test_client, init_database): """ 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'Unable to login' in response.data + assert b'Can\'t find user' in response.data def test_login_email_missing(test_client, init_database): @@ -407,7 +414,7 @@ def test_set_admin_admin1_logged_in_user_not_exist(test_client, init_database): headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))}, content_type='application/json') assert response.status_code == 500 - assert b'Unable to update user' in response.data + assert b'Can\'t find user' in response.data def test_set_admin_admin1_logged_in_email_missing(test_client, init_database): @@ -466,6 +473,94 @@ def test_set_admin_admin1_logged_in_admin_empty(test_client, init_database): 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' diff --git a/api/tests/unit/__init__.py b/api/tests/unit/__init__.py index e69de29..dfaac6b 100644 --- a/api/tests/unit/__init__.py +++ 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 index cf35577..110e7ae 100644 --- a/api/tests/unit/test_auth.py +++ b/api/tests/unit/test_auth.py @@ -1,3 +1,9 @@ +__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. """ diff --git a/api/tests/unit/test_helper_functions.py b/api/tests/unit/test_helper_functions.py index 5cf054b..0c61bcf 100644 --- a/api/tests/unit/test_helper_functions.py +++ b/api/tests/unit/test_helper_functions.py @@ -1,3 +1,9 @@ +__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. """ @@ -18,3 +24,10 @@ def test_check_password(): 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 index e8a336a..57618a9 100644 --- a/api/tests/unit/test_models.py +++ b/api/tests/unit/test_models.py @@ -1,9 +1,14 @@ +__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.models import User, Transaction, Keyword, Share - from app.helper_functions import hash_password +from app.models import User, Transaction, Keyword, Share def test_new_user(): @@ -16,14 +21,15 @@ def test_new_user(): email="user@example.com", username="user", password=hash_password("password"), - admin=False + 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() == {'email': 'user@example.com', 'username': 'user', 'telegram_user_id': None, 'admin': 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): diff --git a/api/tests/unit/test_transaction.py b/api/tests/unit/test_transaction.py index 66f51ae..91f8e25 100644 --- a/api/tests/unit/test_transaction.py +++ b/api/tests/unit/test_transaction.py @@ -1,3 +1,9 @@ +__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. """ diff --git a/api/tests/unit/test_user.py b/api/tests/unit/test_user.py index 91ad580..2846fe7 100644 --- a/api/tests/unit/test_user.py +++ b/api/tests/unit/test_user.py @@ -1,3 +1,9 @@ +__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. """ @@ -38,3 +44,12 @@ def test_check_if_admin_data_exists(): 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/aktienbot/docker-compose.yml b/deploy/aktienbot/docker-compose.yml index a485892..46aee48 100644 --- a/deploy/aktienbot/docker-compose.yml +++ b/deploy/aktienbot/docker-compose.yml @@ -5,7 +5,7 @@ services: image: registry.flokaiser.com/aktienbot/frontend labels: traefik.enable: 'true' - traefik.http.routers.aktienbot_fe.rule: Host(`gruppe1.testsites.info`) + 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 diff --git a/deploy/base/docker-compose.yml b/deploy/base/docker-compose.yml index 6cd9531..0782888 100644 --- a/deploy/base/docker-compose.yml +++ b/deploy/base/docker-compose.yml @@ -12,6 +12,41 @@ services: - ${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 @@ -36,3 +71,5 @@ networks: 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/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/frontend/src/app/Helpers/helper.service.ts b/frontend/src/app/Helpers/helper.service.ts index ac7ca1c..60a57c6 100644 --- a/frontend/src/app/Helpers/helper.service.ts +++ b/frontend/src/app/Helpers/helper.service.ts @@ -1,10 +1,43 @@ import { Injectable } from '@angular/core'; -import { Stock } from '../Models/stock.model'; +import { BotService } from '../Services/bot.service'; +import { Keyword, Share } from '../Views/bot-settings/bot-settings.component'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HelperService { + constructor(private botService: BotService) {} - constructor() { } + /** + * @param {number} ms + */ + delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + formatShareData(): Share[] { + var shares: Share[] = []; + this.botService.getSymbols().subscribe((result) => { + var data = JSON.parse(result); + for (let i = 0; i < data.data.length; i++) { + shares.push({ + symbol: data.data[i].symbol, + }); + } + }); + return shares; + } + + formatKeywordsData(): Keyword[] { + var keywords: Keyword[] = []; + this.botService.getKeywords().subscribe((result) => { + var data = JSON.parse(result); + for (let i = 0; i < data.data.length; i++) { + keywords.push({ + name: data.data[i].keyword, + }); + } + }); + return keywords; + } } diff --git a/frontend/src/app/Models/stock.model.ts b/frontend/src/app/Models/stock.model.ts index 4220de6..e69de29 100644 --- a/frontend/src/app/Models/stock.model.ts +++ b/frontend/src/app/Models/stock.model.ts @@ -1,6 +0,0 @@ -export class Stock { - count = 0; - price = 0; - symbol = ''; - time = ''; -} diff --git a/frontend/src/app/Services/auth.service.ts b/frontend/src/app/Services/auth.service.ts index c5de4fb..c011e2c 100644 --- a/frontend/src/app/Services/auth.service.ts +++ b/frontend/src/app/Services/auth.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; -const AUTH_API = 'https://aktienbot.flokaiser.com/api/user/'; +const AUTH_API = 'https://gruppe1.testsites.info/api/user'; + const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), }; @@ -11,12 +12,25 @@ const httpOptions = { }) export class AuthService { constructor(private http: HttpClient) {} + + /** + * @param {string} email + * @param {string} password + * @returns Observable + */ login(email: string, password: string): Observable { return this.http.post(AUTH_API + '/login', { email, password, }); } + + /** + * @param {string} email + * @param {string} username + * @param {string} password + * @returns Observable + */ register(email: string, username: string, password: string): Observable { return this.http.post( AUTH_API + '/register', diff --git a/frontend/src/app/Services/bot.service.spec.ts b/frontend/src/app/Services/bot.service.spec.ts new file mode 100644 index 0000000..6b208f5 --- /dev/null +++ b/frontend/src/app/Services/bot.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BotService } from './bot.service'; + +describe('BotService', () => { + let service: BotService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BotService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/Services/bot.service.ts b/frontend/src/app/Services/bot.service.ts new file mode 100644 index 0000000..0e764cd --- /dev/null +++ b/frontend/src/app/Services/bot.service.ts @@ -0,0 +1,112 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { TokenStorageService } from './token.service'; + +const API_URL = 'https://gruppe1.testsites.info/api/'; + +@Injectable({ + providedIn: 'root', +}) +export class BotService { + constructor( + private http: HttpClient, + private tokenStorage: TokenStorageService + ) {} + + /** + * @returns Observable + */ + public getKeywords(): Observable { + return this.http.get(API_URL + 'keywords', { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + responseType: 'text', + }); + } + + /** + * @param {string} keyword + * @returns Observable + */ + public createKeyword(keyword: string): Observable { + return this.http.post( + API_URL + 'keyword', + { + keyword, + }, + { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + } + ); + } + + /** + * @param {string} keyword + * @returns Observable + */ + public deleteKeyword(keyword: string): Observable { + return this.http.delete(API_URL + 'keyword', { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + body: { + keyword, + }, + }); + } + + /** + * @returns Observable + */ + public getSymbols(): Observable { + return this.http.get(API_URL + 'shares', { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + responseType: 'text', + }); + } + + /** + * @param {string} keyword + * @returns Observable + */ + public createShare(symbol: string): Observable { + return this.http.post( + API_URL + 'share', + { + symbol, + }, + { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + } + ); + } + + /** + * @param {string} share + * @returns Observable + */ + public deleteShare(share: string): Observable { + return this.http.delete(API_URL + 'share', { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + body: { + share, + }, + }); + } +} diff --git a/frontend/src/app/Services/data.service.ts b/frontend/src/app/Services/data.service.ts index bbd1863..46fcc10 100644 --- a/frontend/src/app/Services/data.service.ts +++ b/frontend/src/app/Services/data.service.ts @@ -2,16 +2,23 @@ import { Injectable, OnInit } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { delay, Observable } from 'rxjs'; import { TokenStorageService } from './token.service'; -const API_URL = 'https://aktienbot.flokaiser.com/api/'; +const API_URL = 'https://gruppe1.testsites.info/api/'; @Injectable({ providedIn: 'root', }) export class DataService { + /** + * @param {HttpClient} privatehttp + * @param {TokenStorageService} privatetokenStorage + */ constructor( private http: HttpClient, private tokenStorage: TokenStorageService ) {} + /** + * @returns Observable + */ public getStockData(): Observable { return this.http.get(API_URL + 'portfolio', { headers: new HttpHeaders({ @@ -22,6 +29,9 @@ export class DataService { }); } + /** + * @returns Observable + */ public getTransactionData(): Observable { return this.http.get(API_URL + 'transactions', { headers: new HttpHeaders({ @@ -32,6 +42,41 @@ export class DataService { }); } + /** + * @param {string} symbol + * @param {Date} time + * @param {number} count + * @param {number} price + * @returns Observable + */ + public createTransaction( + symbol: string, + time: string, + count: number, + price: number + ): Observable { + time = time + 'T12:00:00.000Z'; + price.toFixed(2); + return this.http.post( + API_URL + 'transaction', + { + count, + price, + symbol, + time, + }, + { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + } + ); + } + + /** + * @returns Observable + */ public getKeywords(): Observable { return this.http.get(API_URL + 'keywords', { headers: new HttpHeaders({ diff --git a/frontend/src/app/Services/profile.service.spec.ts b/frontend/src/app/Services/profile.service.spec.ts new file mode 100644 index 0000000..2ddf7f2 --- /dev/null +++ b/frontend/src/app/Services/profile.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ProfileService } from './profile.service'; + +describe('ProfileService', () => { + let service: ProfileService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ProfileService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/Services/profile.service.ts b/frontend/src/app/Services/profile.service.ts new file mode 100644 index 0000000..b23a9b3 --- /dev/null +++ b/frontend/src/app/Services/profile.service.ts @@ -0,0 +1,69 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { TokenStorageService } from './token.service'; + +const API_URL = 'https://gruppe1.testsites.info/api/'; + +@Injectable({ + providedIn: 'root', +}) +export class ProfileService { + constructor( + private tokenStorage: TokenStorageService, + private http: HttpClient + ) {} + + /** + * @returns Observable + */ + public getUserData(): Observable { + return this.http.get(API_URL + 'user', { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + responseType: 'text', + }); + } + + /** + * @param {string} username + * @param {number} password + * @returns Observable + */ + public updateProfile(username: string, password: number): Observable { + return this.http.put( + API_URL + 'user', + { + username, + password, + }, + { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + } + ); + } + + /** + * @param {string} telegramUserID + * @returns Observable + */ + public addTelegramId(telegram_user_id: string): Observable { + return this.http.post( + API_URL + 'telegram', + { + telegram_user_id, + }, + { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.tokenStorage.getToken(), + }), + } + ); + } +} diff --git a/frontend/src/app/Services/token.service.spec.ts b/frontend/src/app/Services/token.service.spec.ts index 7930902..bff5e17 100644 --- a/frontend/src/app/Services/token.service.spec.ts +++ b/frontend/src/app/Services/token.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { TokenService } from './token.service'; +import { TokenStorageService } from './token.service'; -describe('TokenService', () => { - let service: TokenService; +describe('TokenStorageService', () => { + let service: TokenStorageService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(TokenService); + service = TestBed.inject(TokenStorageService); }); it('should be created', () => { diff --git a/frontend/src/app/Services/token.service.ts b/frontend/src/app/Services/token.service.ts index f56b112..8460ed6 100644 --- a/frontend/src/app/Services/token.service.ts +++ b/frontend/src/app/Services/token.service.ts @@ -6,20 +6,42 @@ const USER_KEY = 'auth-user'; }) export class TokenStorageService { constructor() {} + + /** + * @returns void + */ signOut(): void { window.sessionStorage.clear(); } + + /** + * @param {string} token + * @returns void + */ public saveToken(token: string): void { window.sessionStorage.removeItem(TOKEN_KEY); window.sessionStorage.setItem(TOKEN_KEY, token); } + + /** + * @returns string + */ public getToken(): string | null { return window.sessionStorage.getItem(TOKEN_KEY); } + + /** + * @param {any} user + * @returns void + */ public saveUser(user: any): void { window.sessionStorage.removeItem(USER_KEY); window.sessionStorage.setItem(USER_KEY, JSON.stringify(user)); } + + /** + * @returns any + */ public getUser(): any { const user = window.sessionStorage.getItem(USER_KEY); if (user) { diff --git a/frontend/src/app/Views/bot-settings/bot-settings.component.html b/frontend/src/app/Views/bot-settings/bot-settings.component.html index 58d7a42..8318bb9 100644 --- a/frontend/src/app/Views/bot-settings/bot-settings.component.html +++ b/frontend/src/app/Views/bot-settings/bot-settings.component.html @@ -1 +1,58 @@ -

bot-settings works!

+ + + + Keywords + + + Keywords + + + {{ keyword.name }} + + + + + + + + + + + Shares + + + Shares + + + {{ share.symbol }} + + + + + + + + + diff --git a/frontend/src/app/Views/bot-settings/bot-settings.component.scss b/frontend/src/app/Views/bot-settings/bot-settings.component.scss index e69de29..aee2dde 100644 --- a/frontend/src/app/Views/bot-settings/bot-settings.component.scss +++ b/frontend/src/app/Views/bot-settings/bot-settings.component.scss @@ -0,0 +1,26 @@ +.form { + width: 100%; +} + +.card { + width: 90%; + height: 80%; + margin: 5%; +} + +.example-full-width { + width: 100%; +} + +.card-title { + padding-bottom: 2.5vh; +} + +mat-grid { + width: 100%; + height: 100%; +} + +.example-chip-list { + width: 100%; +} diff --git a/frontend/src/app/Views/bot-settings/bot-settings.component.ts b/frontend/src/app/Views/bot-settings/bot-settings.component.ts index 40bd01b..6974b24 100644 --- a/frontend/src/app/Views/bot-settings/bot-settings.component.ts +++ b/frontend/src/app/Views/bot-settings/bot-settings.component.ts @@ -1,15 +1,102 @@ import { Component, OnInit } from '@angular/core'; +import { C, COMMA, ENTER, F } from '@angular/cdk/keycodes'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { BotService } from 'src/app/Services/bot.service'; +import { HelperService } from 'src/app/Helpers/helper.service'; + +export interface Fruit { + name: string; +} + +export interface Share { + symbol: string; +} + +export interface Keyword { + name: string; +} @Component({ selector: 'app-bot-settings', templateUrl: './bot-settings.component.html', - styleUrls: ['./bot-settings.component.scss'] + styleUrls: ['./bot-settings.component.scss'], }) export class BotSettingsComponent implements OnInit { + keywords: Keyword[] = []; + shares: Share[] = []; - constructor() { } + constructor(private botService: BotService, private helper: HelperService) {} ngOnInit(): void { + this.shares = this.helper.formatShareData(); + this.keywords = this.helper.formatKeywordsData(); } + addOnBlur = true; + readonly separatorKeysCodes = [ENTER, COMMA] as const; + + async addKeyword(event: MatChipInputEvent): Promise { + const value = (event.value || '').trim(); + + // Add keyword to database + if (value && !this.keywords.includes({ name: value })) { + console.log('Added: ' + value); + this.botService.createKeyword(value).subscribe((result) => { + console.log(result); + }); + } + + // Clear the input value + event.chipInput!.clear(); + + if (value) { + await this.helper.delay(1000); + this.keywords = []; + this.keywords = this.helper.formatKeywordsData(); + } + } + + async removeKeyword(keyword: Keyword): Promise { + this.botService.deleteKeyword(keyword.name).subscribe((result) => { + console.log(result); + }); + + await this.helper.delay(1000); + + this.keywords = []; + this.keywords = this.helper.formatKeywordsData(); + } + + async addShare(event: MatChipInputEvent): Promise { + const value = (event.value || '').trim(); + + // Add share to database + if (value && !this.shares.includes({ symbol: value })) { + console.log('Added: ' + value); + this.botService.createShare(value).subscribe((result) => { + console.log(result); + }); + } + + // Clear the input value + event.chipInput!.clear(); + + if (value) { + await this.helper.delay(1000); + + this.shares = []; + this.shares = this.helper.formatShareData(); + } + } + + async removeShare(share: Share): Promise { + this.botService.deleteShare(share.symbol).subscribe((result) => { + console.log(result); + }); + + await this.helper.delay(1000); + + this.shares = []; + this.shares = this.helper.formatShareData(); + } } diff --git a/frontend/src/app/Views/dashboard/dashboard.component.html b/frontend/src/app/Views/dashboard/dashboard.component.html index 5c1b401..509ebbe 100644 --- a/frontend/src/app/Views/dashboard/dashboard.component.html +++ b/frontend/src/app/Views/dashboard/dashboard.component.html @@ -3,100 +3,148 @@
-
Aktienübersicht
- - +
Stocks
-
- - + +
+
+ - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - -
Symbol{{ element.position }}Symbol{{ element.symbol }}Name{{ element.name }}Count{{ element.count }}Volume{{ element.weight }}Time{{ element.time }}Worth{{ element.symbol }}Current Price + {{ element.currentPrice }} +
-
+ + + +
+
-
Depotübersicht
+
Depot
- + +
+
+
+
+

Portfolio Value

+
+
+
+
+

Portfolio Cost

+
+
+
+
+

Portfolio Profit

+
+
+
+
+
+
+ savings{{ depotCurrentValue.toFixed(2) }} +
+
+ paid{{ depotCost.toFixed(2) }} +
+
+ account_balance{{ profit.toFixed(2) }} +
+
+
+
-
+
-
Transaktionen
+
Transactions
+ +
-
- - - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - -
Count{{ element.position }}Count{{ element.count }}Pirce{{ element.name }}Price{{ element.price }}Symbol{{ element.weight }}Symbol{{ element.symbol }}Time{{ element.symbol }}Time{{ element.time }}
-
+ + +
diff --git a/frontend/src/app/Views/dashboard/dashboard.component.scss b/frontend/src/app/Views/dashboard/dashboard.component.scss index 7bc1360..a2a4c7f 100644 --- a/frontend/src/app/Views/dashboard/dashboard.component.scss +++ b/frontend/src/app/Views/dashboard/dashboard.component.scss @@ -13,6 +13,14 @@ margin-top: 10%; margin-left: 5%; margin-right: 10%; + text-align: center; +} + +.depotOverviewDown { + height: 100%; + width: 100%; + margin-left: 5%; + margin-right: 10%; } .stockTable { @@ -21,12 +29,20 @@ width: 100%; } +.stockTableLHS { + overflow: auto; + height: 83%; + width: 100%; +} + .heading { font-size: xx-large; height: 10%; width: 100%; position: relative; display: flex; + justify-content: center; + align-items: center; } .fix-right-side { @@ -47,7 +63,6 @@ .add-icon { transform: scale(2); - margin-top: 2%; outline: none !important; } @@ -62,3 +77,37 @@ table { .placeholder { height: 100%; } + +.placeholderRHS { + height: 80%; +} + +.mat-ripple-element { + display: none !important; +} + +.money { + margin-left: 2vw; +} + +.green { + color: green; +} + +.red { + color: red; +} + +.row { + height: 20%; +} + +.content { + height: inherit; +} + +.content-container { + width: 100%; + display: grid; + align-items: center; +} diff --git a/frontend/src/app/Views/dashboard/dashboard.component.ts b/frontend/src/app/Views/dashboard/dashboard.component.ts index 27a6140..f3e012b 100644 --- a/frontend/src/app/Views/dashboard/dashboard.component.ts +++ b/frontend/src/app/Views/dashboard/dashboard.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; -import { throwToolbarMixedModesError } from '@angular/material/toolbar'; import { DataService } from 'src/app/Services/data.service'; -import { TokenStorageService } from 'src/app/Services/token.service'; +import { MatDialog } from '@angular/material/dialog'; +import { UserDialogComponent } from './user-dialog/user-dialog.component'; +import { C } from '@angular/cdk/keycodes'; +import { HelperService } from 'src/app/Helpers/helper.service'; export interface PeriodicElement { name: string; @@ -11,11 +13,10 @@ export interface PeriodicElement { } export interface Stock { + count: number; + currentPrice: number; symbol: string; - count: Float32Array; - lastTransaction: Date; - boughtPrice: Float32Array; - currentPrice: Float32Array; + time: string; } //symbol count lastTransaction boughtPrice currentPrice(+?) @@ -24,43 +25,99 @@ const ELEMENT_DATA: PeriodicElement[] = [ { position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' }, { position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' }, { position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' }, - { position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' }, - { position: 5, name: 'Boron', weight: 10.811, symbol: 'B' }, - { position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' }, - { position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' }, - { position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' }, - { position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' }, - { position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' }, - { position: 11, name: 'Hydrogen', weight: 1.0079, symbol: 'H' }, - { position: 12, name: 'Helium', weight: 4.0026, symbol: 'He' }, - { position: 13, name: 'Lithium', weight: 6.941, symbol: 'Li' }, - { position: 14, name: 'Beryllium', weight: 9.0122, symbol: 'Be' }, - { position: 15, name: 'Boron', weight: 10.811, symbol: 'B' }, - { position: 16, name: 'Carbon', weight: 12.0107, symbol: 'C' }, - { position: 17, name: 'Nitrogen', weight: 14.0067, symbol: 'N' }, - { position: 18, name: 'Oxygen', weight: 15.9994, symbol: 'O' }, - { position: 19, name: 'Fluorine', weight: 18.9984, symbol: 'F' }, - { position: 20, name: 'Neon', weight: 20.1797, symbol: 'Ne' }, ]; +var TRANSACTION_DATA: TransactionData[] = []; +var STOCK_DATA: Stock[] = []; + +export interface TransactionData { + symbol: string; + time: string; + count: number; + price: number; +} + @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent implements OnInit { - constructor(private dataService: DataService) {} + constructor( + private helper: HelperService, + private dataService: DataService, + public dialog: MatDialog + ) {} + + dataSourceTransactions: TransactionData[] = []; + dataSourceStocks: Stock[] = []; + depotCurrentValue: number = 0; + depotCost: number = 0; + profit: number = 0; + ngOnInit() { this.dataService.getStockData().subscribe((response: any) => { - console.log(response); - //TODO map data on array for display + var data = JSON.parse(response); + for (let i = 0; i < data.data.length; i++) { + this.depotCurrentValue += data.data[i].current_price; + STOCK_DATA.push({ + count: data.data[i].count, + currentPrice: data.data[i].current_price, + symbol: data.data[i].symbol, + time: data.data[i].last_transaction, + }); + } + this.dataSourceStocks = STOCK_DATA; + //TODO move to helper service + + this.profit += this.depotCurrentValue; }); + this.dataService.getTransactionData().subscribe((response: any) => { - console.log(response); - //TODO map data on array for display + var data = JSON.parse(response); + for (let i = 0; i < data.data.length; i++) { + this.depotCost += data.data[i].price; + TRANSACTION_DATA.push({ + symbol: data.data[i].symbol, + time: data.data[i].time, + count: data.data[i].count, + price: data.data[i].price, + }); + } + this.dataSourceTransactions = TRANSACTION_DATA; + //TODO move to helper service + + this.profit -= this.depotCost; }); } - displayedColumns: string[] = ['position', 'name', 'weight', 'symbol']; + symbol: string = ''; + time: Date = new Date(); + count: number = 0.0; + price: number = 0.0; + + openDialog(): void { + const dialogRef = this.dialog.open(UserDialogComponent, { + width: '50vw', + data: { + symbol: this.symbol, + time: this.time, + count: this.count, + price: this.price, + }, + }); + + dialogRef.afterClosed().subscribe((result) => { + console.log('The dialog was closed'); + }); + } + + displayedColumns: string[] = ['weight', 'position', 'name', 'symbol']; + displayedColumnsStocks: string[] = [ + 'position', + 'name', + 'weight', + 'current-price', + ]; dataSource = ELEMENT_DATA; } diff --git a/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.html b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.html new file mode 100644 index 0000000..21b87cf --- /dev/null +++ b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.html @@ -0,0 +1,61 @@ +

Neue Transaktion hinzufügen

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
diff --git a/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.scss b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.scss new file mode 100644 index 0000000..80e25e5 --- /dev/null +++ b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.scss @@ -0,0 +1,9 @@ +.spacer { + flex-grow: 1; + width: 5%; +} + +.footer-buttons { + display: flex; + width: 100%; +} diff --git a/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.spec.ts b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.spec.ts new file mode 100644 index 0000000..4db6f9c --- /dev/null +++ b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserDialogComponent } from './user-dialog.component'; + +describe('UserDialogComponent', () => { + let component: UserDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.ts b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.ts new file mode 100644 index 0000000..bc73c9b --- /dev/null +++ b/frontend/src/app/Views/dashboard/user-dialog/user-dialog.component.ts @@ -0,0 +1,42 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { + MatDialog, + MatDialogRef, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import { DataService } from 'src/app/Services/data.service'; + +import { TransactionData } from '../dashboard.component'; + +@Component({ + selector: 'app-user-dialog', + templateUrl: './user-dialog.component.html', + styleUrls: ['./user-dialog.component.scss'], +}) +export class UserDialogComponent implements OnInit { + constructor( + private dataService: DataService, + public dialog: MatDialog, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: TransactionData + ) {} + + ngOnInit(): void {} + + onSubmit() { + //TODO check tat price is decimal + console.log( + this.dataService + .createTransaction( + this.data.symbol, + this.data.time, + +this.data.count, + +this.data.price.toFixed(2) + ) + .subscribe((data) => { + console.log(data); + }) + ); + this.dialog.closeAll(); + } +} diff --git a/frontend/src/app/Views/header/header.component.html b/frontend/src/app/Views/header/header.component.html index d72ba56..ecf3ffb 100644 --- a/frontend/src/app/Views/header/header.component.html +++ b/frontend/src/app/Views/header/header.component.html @@ -1,5 +1,5 @@ - Aktienbot + Aktienbot + +
+ +
+ + diff --git a/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.scss b/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.scss new file mode 100644 index 0000000..b960d19 --- /dev/null +++ b/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.scss @@ -0,0 +1,18 @@ +.footer-buttons { + width: 100%; + text-align: center; +} + +.spacer { + flex-grow: 1; + width: 5%; +} + +.inner { + display: inline-block; + width: 50%; +} + +.content { + height: 80%; +} diff --git a/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.spec.ts b/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.spec.ts new file mode 100644 index 0000000..80bb525 --- /dev/null +++ b/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfirmationDialogComponent } from './confirmation-dialog.component'; + +describe('ConfirmationDialogComponent', () => { + let component: ConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConfirmationDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.ts b/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.ts new file mode 100644 index 0000000..7b5445a --- /dev/null +++ b/frontend/src/app/Views/profile/confirmation-dialog/confirmation-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-confirmation-dialog', + templateUrl: './confirmation-dialog.component.html', + styleUrls: ['./confirmation-dialog.component.scss'], +}) +export class ConfirmationDialogComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} + + confirm() {} + + returnBack() {} +} diff --git a/frontend/src/app/Views/profile/profile.component.html b/frontend/src/app/Views/profile/profile.component.html index 9df0576..37a5180 100644 --- a/frontend/src/app/Views/profile/profile.component.html +++ b/frontend/src/app/Views/profile/profile.component.html @@ -1 +1,135 @@ -

profile works!

+ + + + Profile Information + +
+ + Username + + + Username is required + + + + {{ form.email }} + + + + Password + + + Please enter a valid password + + + Password is required + + + + Repeat Password + + + Please enter a valid password + + + Password is required + + + +
+
+
+
+ + + Add Telegram Id + +
+ + Telegram UserId + + + Id is required + + + +
+
+
+
+
diff --git a/frontend/src/app/Views/profile/profile.component.scss b/frontend/src/app/Views/profile/profile.component.scss index e69de29..d30d37b 100644 --- a/frontend/src/app/Views/profile/profile.component.scss +++ b/frontend/src/app/Views/profile/profile.component.scss @@ -0,0 +1,22 @@ +.form { + width: 100%; +} + +.card { + width: 90%; + height: 80%; + margin: 5%; +} + +.example-full-width { + width: 100%; +} + +.card-title { + padding-bottom: 2.5vh; +} + +mat-grid { + width: 100%; + height: 100%; +} diff --git a/frontend/src/app/Views/profile/profile.component.ts b/frontend/src/app/Views/profile/profile.component.ts index 89b667f..795d4ba 100644 --- a/frontend/src/app/Views/profile/profile.component.ts +++ b/frontend/src/app/Views/profile/profile.component.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { FormControl, PatternValidator, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { ProfileService } from 'src/app/Services/profile.service'; +import { ConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component'; @Component({ selector: 'app-profile', @@ -6,7 +10,69 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./profile.component.scss'], }) export class ProfileComponent implements OnInit { - constructor() {} + userNameFormControl = new FormControl('', [Validators.required]); + passwordFormControl = new FormControl('', [ + Validators.required, + Validators.minLength(6), + ]); + telegramIdFormControl = new FormControl('', [Validators.required]); - ngOnInit(): void {} + userId = ''; + + form: any = { + username: null, + email: 'example@web.com', + password: 'password', + }; + + constructor( + private profileService: ProfileService, + public dialog: MatDialog + ) {} + + ngOnInit(): void { + this.profileService.getUserData().subscribe((result) => { + console.log(result); + result = JSON.parse(result); + this.form.username = result.data.username; + this.form.password = result.data.password; + this.form.email = result.data.email; + this.userId = result.data.telegram_user_id; + }); + } + + onSubmit() { + if (this.userId != '') { + console.log(this.userId); + this.profileService.addTelegramId(this.userId).subscribe((result) => { + console.log(result); + }); + } + } + + updateUser() { + const { username, email, password } = this.form; + this.profileService + .updateProfile(this.form.username, this.form.password) + .subscribe((result) => { + console.log(result); + }); + } + + openDialog(action: string) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + width: '50vw', + height: '20vh', + }); + + dialogRef.afterClosed().subscribe((result) => { + if (result === true) { + if (action === 'addTelegram') { + this.onSubmit(); + } else if (action === 'updateUser') { + this.updateUser(); + } + } + }); + } } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 6d05a46..5d53f26 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,5 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { BotService } from './Services/bot.service'; +import { BotSettingsComponent } from './Views/bot-settings/bot-settings.component'; import { DashboardComponent } from './Views/dashboard/dashboard.component'; import { LoginComponent } from './Views/login/login.component'; import { ProfileComponent } from './Views/profile/profile.component'; @@ -24,7 +26,7 @@ const routes: Routes = [ }, { path: 'settings', - component: ProfileComponent, + component: BotSettingsComponent, }, ]; diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index 8b13789..e69de29 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -1 +0,0 @@ - diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 1e925aa..958f243 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -10,6 +10,9 @@ import { MatGridListModule } from '@angular/material/grid-list'; import { MatCardModule } from '@angular/material/card'; import { MatTableModule } from '@angular/material/table'; import { MatMenuModule } from '@angular/material/menu'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatInputModule } from '@angular/material/input'; +import { MatChipsModule } from '@angular/material/chips'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -20,6 +23,8 @@ import { DashboardComponent } from './Views/dashboard/dashboard.component'; import { RegisterComponent } from './Views/register/register.component'; import { ProfileComponent } from './Views/profile/profile.component'; import { BotSettingsComponent } from './Views/bot-settings/bot-settings.component'; +import { UserDialogComponent } from './Views/dashboard/user-dialog/user-dialog.component'; +import { ConfirmationDialogComponent } from './Views/profile/confirmation-dialog/confirmation-dialog.component'; @NgModule({ declarations: [ @@ -30,6 +35,8 @@ import { BotSettingsComponent } from './Views/bot-settings/bot-settings.componen RegisterComponent, ProfileComponent, BotSettingsComponent, + UserDialogComponent, + ConfirmationDialogComponent, ], imports: [ BrowserModule, @@ -44,6 +51,10 @@ import { BotSettingsComponent } from './Views/bot-settings/bot-settings.componen FormsModule, HttpClientModule, MatMenuModule, + MatDialogModule, + MatInputModule, + ReactiveFormsModule, + MatChipsModule, ], providers: [], bootstrap: [AppComponent],