diff --git a/src/bot/startup_migration_extension.py b/src/bot/startup_migration_extension.py index b8f7ff0e37..62d4d39d85 100644 --- a/src/bot/startup_migration_extension.py +++ b/src/bot/startup_migration_extension.py @@ -4,6 +4,7 @@ from cpl_core.dependency_injection import ServiceCollectionABC from cpl_core.environment import ApplicationEnvironmentABC from bot_data.abc.migration_abc import MigrationABC +from bot_data.migration.api_migration import ApiMigration from bot_data.migration.auto_role_migration import AutoRoleMigration from bot_data.migration.initial_migration import InitialMigration from bot_data.service.migration_service import MigrationService @@ -21,3 +22,4 @@ class StartupMigrationExtension(StartupExtensionABC): services.add_transient(MigrationService) services.add_transient(MigrationABC, InitialMigration) services.add_transient(MigrationABC, AutoRoleMigration) # 03.10.2022 #54 - 0.2.2 + services.add_transient(MigrationABC, ApiMigration) # 15.10.2022 #70 - 0.3.0 diff --git a/src/bot_api/api.py b/src/bot_api/api.py index 8760377bbe..6a08c9a612 100644 --- a/src/bot_api/api.py +++ b/src/bot_api/api.py @@ -1,13 +1,16 @@ +import json import sys +import uuid from functools import partial -from blinker import Namespace from cpl_core.dependency_injection import ServiceProviderABC -from flask import Flask, request +from flask import Flask, request, jsonify, Response, make_response from flask_cors import CORS from bot_api.configuration.api_settings import ApiSettings +from bot_api.exception.service_exception import ServiceException from bot_api.logging.api_logger import ApiLogger +from bot_api.model.error_dto import ErrorDTO from bot_api.route.route import Route @@ -33,8 +36,8 @@ class Api(Flask): # register before request self.before_request_funcs.setdefault(None, []).append(self.before_request) - my_signals = Namespace() - notify = my_signals.signal('notify') + exc_class, code = self._get_exc_class_and_code(Exception) + self.error_handler_spec[None][code][exc_class] = self.handle_exception def _register_routes(self): for path, f in Route.registered_routes.items(): @@ -50,6 +53,21 @@ class Api(Flask): partial_f.__name__ = route.__name__ self.route(path, **kwargs)(partial_f) + def handle_exception(self, e: Exception): + self._logger.error(__name__, f'Caught error', e) + + if isinstance(e, ServiceException): + ex: ServiceException = e + self._logger.error(__name__, ex.get_detailed_message()) + error = ErrorDTO(ex.error_code, ex.message) + return jsonify(error.to_dict()), 500 + else: + tracking_id = uuid.uuid4() + user_message = f'Tracking Id: {tracking_id}' + self._logger.error(__name__, user_message, e) + error = ErrorDTO(None, user_message) + return jsonify(error.to_dict()), 400 + def before_request(self, *args, **kwargs): self._logger.debug(__name__, f'Received GET @{request.url}') headers = str(request.headers).replace("\n", "\n\t") diff --git a/src/bot_api/exception/__init__.py b/src/bot_api/exception/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bot_api/exception/service_error_code_enum.py b/src/bot_api/exception/service_error_code_enum.py new file mode 100644 index 0000000000..24da48ed24 --- /dev/null +++ b/src/bot_api/exception/service_error_code_enum.py @@ -0,0 +1,19 @@ +from enum import Enum + + +class ServiceErrorCode(Enum): + + Unknown = 0 + + InvalidDependencies = 1 + InvalidData = 2 + NotFound = 3 + DataAlreadyExists = 4 + UnableToAdd = 5 + UnableToDelete = 6 + + InvalidUser = 7 + + ConnectionFailed = 8 + Timeout = 9 + MailError = 10 diff --git a/src/bot_api/exception/service_exception.py b/src/bot_api/exception/service_exception.py new file mode 100644 index 0000000000..6451ae50ed --- /dev/null +++ b/src/bot_api/exception/service_exception.py @@ -0,0 +1,13 @@ +from bot_api.exception.service_error_code_enum import ServiceErrorCode + + +class ServiceException(Exception): + + def __init__(self, error_code: ServiceErrorCode, message: str, *args): + Exception.__init__(self, *args) + + self.error_code = error_code + self.message = message + + def get_detailed_message(self) -> str: + return f'ServiceException - ErrorCode: {self.error_code} - ErrorMessage: {self.message}' diff --git a/src/bot_api/model/error_dto.py b/src/bot_api/model/error_dto.py new file mode 100644 index 0000000000..b55cc1c508 --- /dev/null +++ b/src/bot_api/model/error_dto.py @@ -0,0 +1,34 @@ +import traceback +from typing import Optional + +from cpl_core.console import Console + +from bot_api.abc.dto_abc import DtoABC +from bot_api.exception.service_error_code_enum import ServiceErrorCode + + +class ErrorDTO(DtoABC): + + def __init__(self, error_code: Optional[ServiceErrorCode], message: str): + DtoABC.__init__(self) + + self._error_code = ServiceErrorCode.Unknown if error_code is None else error_code + self._message = message + + @property + def error_code(self) -> ServiceErrorCode: + return self._error_code + + @property + def message(self) -> str: + return self._message + + def from_dict(self, values: dict): + self._error_code = values['ErrorCode'] + self._message = values['Message'] + + def to_dict(self) -> dict: + return { + 'ErrorCode': int(self._error_code.value), + 'Message': self._message + } diff --git a/src/bot_api/route/route.py b/src/bot_api/route/route.py index fbf1a9f8dc..74fbbc6724 100644 --- a/src/bot_api/route/route.py +++ b/src/bot_api/route/route.py @@ -1,3 +1,6 @@ +from flask_cors import cross_origin + + class Route: registered_routes = {} @@ -5,6 +8,7 @@ class Route: def route(cls, path=None, **kwargs): # simple decorator for class based views def inner(fn): + cross_origin(fn) cls.registered_routes[path] = (fn, kwargs) return fn diff --git a/src/bot_api/service/auth_service.py b/src/bot_api/service/auth_service.py index 472585900b..61b5cca601 100644 --- a/src/bot_api/service/auth_service.py +++ b/src/bot_api/service/auth_service.py @@ -13,6 +13,8 @@ from cpl_translation import TranslatePipe from bot_api.abc.auth_service_abc import AuthServiceABC from bot_api.configuration.authentication_settings import AuthenticationSettings from bot_api.configuration.frontend_settings import FrontendSettings +from bot_api.exception.service_error_code_enum import ServiceErrorCode +from bot_api.exception.service_exception import ServiceException from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria from bot_api.logging.api_logger import ApiLogger from bot_api.model.auth_user_dto import AuthUserDTO @@ -131,7 +133,7 @@ class AuthService(AuthServiceABC): return user except Exception as e: self._logger.error(__name__, f'AuthUser not found', e) - raise Exception(f'User not found {email}') + raise ServiceException(ServiceErrorCode.InvalidData, f'User not found {email}') async def find_auth_user_by_email_async(self, email: str) -> Optional[AuthUser]: user = self._auth_users.find_auth_user_by_email(email) @@ -140,12 +142,12 @@ class AuthService(AuthServiceABC): async def add_auth_user_async(self, user_dto: AuthUserDTO): db_user = self._auth_users.find_auth_user_by_email(user_dto.email) if db_user is not None: - raise Exception('User already exists') + raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists') user_dto.password = self._hash_sha256(user_dto.password) user = AUT.to_db(user_dto) if not self._is_email_valid(user.email): - raise Exception('Invalid E-Mail address') + raise ServiceException(ServiceErrorCode.InvalidData, 'Invalid E-Mail address') try: user.confirmation_id = uuid.uuid4() @@ -155,26 +157,27 @@ class AuthService(AuthServiceABC): self._logger.info(__name__, f'Added auth user with E-Mail: {user_dto.email}') except Exception as e: self._logger.error(__name__, f'Cannot add user with E-Mal {user_dto.email}', e) + raise ServiceException(ServiceErrorCode.UnableToAdd, "Invalid E-Mail") async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): if update_user_dto is None: - raise Exception(f'User is empty') + raise ServiceException(ServiceErrorCode.InvalidData, f'User is empty') if update_user_dto.auth_user is None: - raise Exception(f'Existing user is empty') + raise ServiceException(ServiceErrorCode.InvalidData, f'Existing user is empty') if update_user_dto.new_auth_user is None: - raise Exception(f'New user is empty') + raise ServiceException(ServiceErrorCode.InvalidData, f'New user is empty') if not self._is_email_valid(update_user_dto.auth_user.email) or not self._is_email_valid(update_user_dto.new_auth_user.email): - raise Exception(f'Invalid E-Mail') + raise ServiceException(ServiceErrorCode.InvalidData, f'Invalid E-Mail') user = self._auth_users.find_auth_user_by_email(update_user_dto.auth_user.email) if user is None: - raise Exception('User not found') + raise ServiceException(ServiceErrorCode.InvalidUser, 'User not found') if user.confirmation_id is not None: - raise Exception('E-Mail not confirmed') + raise ServiceException(ServiceErrorCode.InvalidUser, 'E-Mail not confirmed') # update first name if update_user_dto.new_auth_user.first_name is not None and update_user_dto.auth_user.first_name != update_user_dto.new_auth_user.first_name: @@ -189,7 +192,7 @@ class AuthService(AuthServiceABC): if update_user_dto.new_auth_user.email is not None and update_user_dto.new_auth_user.email != '' and update_user_dto.auth_user.email != update_user_dto.new_auth_user.email: user_by_new_e_mail = self._auth_users.find_auth_user_by_email(update_user_dto.new_auth_user.email) if user_by_new_e_mail is not None: - raise Exception('User already exists') + raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists') user.email = update_user_dto.new_auth_user.email is_existing_password_set = False @@ -200,7 +203,7 @@ class AuthService(AuthServiceABC): update_user_dto.auth_user.Password = self._hash_sha256(update_user_dto.auth_user.Password) if update_user_dto.auth_user.Password != user.Password: - raise Exception('Wrong password') + raise ServiceException(ServiceErrorCode.InvalidUser, 'Wrong password') if update_user_dto.new_auth_user.Password is not None and update_user_dto.new_auth_user.Password != '': is_new_password_set = True @@ -214,27 +217,27 @@ class AuthService(AuthServiceABC): async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO): if update_user_dto is None: - raise Exception(f'User is empty') + raise ServiceException(ServiceErrorCode.InvalidData, f'User is empty') if update_user_dto.auth_user is None: - raise Exception(f'Existing user is empty') + raise ServiceException(ServiceErrorCode.InvalidData, f'Existing user is empty') if update_user_dto.new_auth_user is None: - raise Exception(f'New user is empty') + raise ServiceException(ServiceErrorCode.InvalidData, f'New user is empty') if not self._is_email_valid(update_user_dto.auth_user.email) or not self._is_email_valid(update_user_dto.new_auth_user.email): - raise Exception(f'Invalid E-Mail') + raise ServiceException(ServiceErrorCode.InvalidData, f'Invalid E-Mail') user = self._auth_users.find_auth_user_by_email(update_user_dto.auth_user.email) if user is None: - raise Exception('User not found') + raise ServiceException(ServiceErrorCode.InvalidUser, 'User not found') if user.ConfirmationId is not None and update_user_dto.new_auth_user.is_confirmed: user.ConfirmationId = None elif user.ConfirmationId is None and not update_user_dto.new_auth_user.is_confirmed: user.confirmation_id = uuid.uuid4() # else - # raise Exception(ServiceErrorCode.InvalidUser, 'E-Mail not confirmed') + # raise ServiceException(ServiceErrorCode.InvalidUser, 'E-Mail not confirmed') # update first name if update_user_dto.new_auth_user.first_name is not None and update_user_dto.auth_user.first_name != update_user_dto.new_auth_user.first_name: @@ -248,7 +251,7 @@ class AuthService(AuthServiceABC): if update_user_dto.new_auth_user.email is not None and update_user_dto.new_auth_user.email != '' and update_user_dto.auth_user.email != update_user_dto.new_auth_user.email: user_by_new_e_mail = self._auth_users.find_auth_user_by_email(update_user_dto.new_auth_user.email) if user_by_new_e_mail is not None: - raise Exception('User already exists') + raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists') user.EMail = update_user_dto.new_auth_user.email # update password @@ -268,7 +271,7 @@ class AuthService(AuthServiceABC): self._db.save_changes() except Exception as e: self._logger.error(__name__, f'Cannot delete user', e) - raise Exception(f'Cannot delete user by mail {email}') + raise ServiceException(ServiceErrorCode.UnableToDelete, f'Cannot delete user by mail {email}') async def delete_auth_user_async(self, user_dto: AuthUserDTO): try: @@ -276,21 +279,21 @@ class AuthService(AuthServiceABC): self._db.save_changes() except Exception as e: self._logger.error(__name__, f'Cannot delete user', e) - raise Exception(f'Cannot delete user by mail {user_dto.email}') + raise ServiceException(ServiceErrorCode.UnableToDelete, f'Cannot delete user by mail {user_dto.email}') async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: if user_dto is None: - raise Exception('User not set') + raise ServiceException(ServiceErrorCode.InvalidData, 'User not set') db_user = self._auth_users.find_auth_user_by_email(user_dto.email) if db_user is None: - raise Exception(f'User with E-Mail {user_dto.email} not found') + raise ServiceException(ServiceErrorCode.InvalidUser, f'User not found') user_dto.password = self._hash_sha256(user_dto.password) if db_user.password != user_dto.password: - raise Exception('Wrong password') + raise ServiceException(ServiceErrorCode.InvalidUser, 'Wrong password') - token = self._generate_token(user_dto) + token = self._generate_token(db_user) refresh_token = self._create_and_save_refresh_token(db_user) if db_user.forgot_password_id is not None: db_user.forgot_password_id = None @@ -300,16 +303,16 @@ class AuthService(AuthServiceABC): async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO: if token_dto is None: - raise Exception(f'Token not set') + raise ServiceException(ServiceErrorCode.InvalidData, f'Token not set') token = jwt.decode(token_dto.token, key=self._auth_settings.secret_key) if token is None or 'email' not in token: - raise Exception('Token invalid') + raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid') try: user = self._auth_users.get_auth_user_by_email(token) if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): - raise Exception('Token expired') + raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired') return TokenDTO(self._generate_token(user), self._create_and_save_refresh_token(user)) except Exception as e: @@ -318,13 +321,13 @@ class AuthService(AuthServiceABC): async def revoke_async(self, token_dto: TokenDTO): if token_dto is None or token_dto.token is None or token_dto.refresh_token is None: - raise Exception('Token not set') + raise ServiceException(ServiceErrorCode.InvalidData, 'Token not set') token = jwt.decode(token_dto.token, key=self._auth_settings.secret_key) try: user = self._auth_users.get_auth_user_by_email(token) if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): - raise Exception('Token expired') + raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired') user.refresh_token = None self._auth_users.update_auth_user(user) @@ -359,13 +362,13 @@ class AuthService(AuthServiceABC): async def reset_password_async(self, rp_dto: ResetPasswordDTO): user = self._auth_users.find_auth_user_by_forgot_password_id(rp_dto.id) if user is None: - raise Exception(f'User by forgot password id {rp_dto.id} not found') + raise ServiceException(ServiceErrorCode.InvalidUser, f'User by forgot password id {rp_dto.id} not found') if user.confirmation_id is not None: - raise Exception(f'E-Mail not confirmed') + raise ServiceException(ServiceErrorCode.InvalidUser, f'E-Mail not confirmed') if user.password is None or rp_dto.password == '': - raise Exception(f'Password not set') + raise ServiceException(ServiceErrorCode.InvalidData, f'Password not set') user.password = self._hash_sha256(rp_dto.password) self._db.save_changes() diff --git a/src/bot_data/migration/api_migration.py b/src/bot_data/migration/api_migration.py index 187dc5fcbe..61f9d15615 100644 --- a/src/bot_data/migration/api_migration.py +++ b/src/bot_data/migration/api_migration.py @@ -27,9 +27,9 @@ class ApiMigration(MigrationABC): `ConfirmationId` varchar(255) DEFAULT NULL, `ForgotPasswordId` varchar(255) DEFAULT NULL, `RefreshTokenExpiryTime` datetime(6) NOT NULL, - `AuthRole` int NOT NULL DEFAULT '0' + `AuthRole` int NOT NULL DEFAULT '0', `CreatedOn` datetime(6) NOT NULL, - `LastModifiedOn` datetime(6) NOT NULL, + `LastModifiedOn` datetime(6) NOT NULL ) """) ) diff --git a/src/bot_data/model/auth_user.py b/src/bot_data/model/auth_user.py index eeb90d76e0..57cdde0aeb 100644 --- a/src/bot_data/model/auth_user.py +++ b/src/bot_data/model/auth_user.py @@ -132,21 +132,21 @@ class AuthUser(TableABC): def get_select_by_email_string(email: str) -> str: return str(f""" SELECT * FROM `AuthUsers` - WHERE `EMail` = {email}; + WHERE `EMail` = '{email}'; """) @staticmethod def get_select_by_confirmation_id_string(id: str) -> str: return str(f""" SELECT * FROM `AuthUsers` - WHERE `ConfirmationId` = {id}; + WHERE `ConfirmationId` = '{id}'; """) @staticmethod def get_select_by_forgot_password_i_string(id: str) -> str: return str(f""" SELECT * FROM `AuthUsers` - WHERE `ForgotPasswordId` = {id}; + WHERE `ForgotPasswordId` = '{id}'; """) @property @@ -167,17 +167,17 @@ class AuthUser(TableABC): `LastModifiedOn` ) VALUES ( {self._auth_user_id}, - {self._first_name}, - {self._last_name}, - {self._email}, - {self._password}, - {self._refresh_token}, - {self._confirmation_id}, - {self._forgot_password_id}, - {self._refresh_token_expire_time}, - {self._auth_role_id.value} - {self._created_at}, - {self._modified_at} + '{self._first_name}', + '{self._last_name}', + '{self._email}', + '{self._password}', + '{self._refresh_token}', + '{self._confirmation_id}', + '{self._forgot_password_id}', + '{self._refresh_token_expire_time}', + {self._auth_role_id.value}, + '{self._created_at}', + '{self._modified_at}' ) """)