diff --git a/src/bot/application.py b/src/bot/application.py index 726cdbdb..6c65a6b4 100644 --- a/src/bot/application.py +++ b/src/bot/application.py @@ -46,7 +46,7 @@ class Application(DiscordBotApplicationABC): if self._feature_flags.get_flag(FeatureFlagsEnum.api_module): self._api.start() - if self._feature_flags.get_flag(FeatureFlagsEnum.api_only): + if self._feature_flags.get_flag(FeatureFlagsEnum.api_only) and self._environment.environment_name == 'development': self._api.join() return diff --git a/src/bot/bot.json b/src/bot/bot.json index 64495674..df826bb7 100644 --- a/src/bot/bot.json +++ b/src/bot/bot.json @@ -18,7 +18,7 @@ "Dependencies": [ "cpl-core==2022.10.0.post6", "cpl-translation==2022.10.0.post1", - "cpl-query==2022.10.0", + "cpl-query==2022.10.0.post1", "cpl-discord==2022.10.0.post5" ], "DevDependencies": [ diff --git a/src/bot/translation/de.json b/src/bot/translation/de.json index 5ffe9dcf..7850782c 100644 --- a/src/bot/translation/de.json +++ b/src/bot/translation/de.json @@ -163,7 +163,7 @@ "auth": { "confirmation": { "subject": "E-Mail für {} {} bestätigen", - "message": "Öffne den Link um die E-Mail zu bestätigen:\n{}auth/forgot-password/{}" + "message": "Öffne den Link um die E-Mail zu bestätigen:\n{}auth/register/{}" }, "forgot_password": { "subject": "Passwort für {} {} zurücksetzen", diff --git a/src/bot_api/abc/auth_service_abc.py b/src/bot_api/abc/auth_service_abc.py index e4f611c6..28d13749 100644 --- a/src/bot_api/abc/auth_service_abc.py +++ b/src/bot_api/abc/auth_service_abc.py @@ -9,7 +9,6 @@ from bot_api.model.email_string_dto import EMailStringDTO from bot_api.model.reset_password_dto import ResetPasswordDTO from bot_api.model.token_dto import TokenDTO from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO -from bot_data.model.auth_user import AuthUser class AuthServiceABC(ABC): @@ -18,16 +17,16 @@ class AuthServiceABC(ABC): def __init__(self): pass @abstractmethod - async def get_all_auth_users_async(self) -> List[AuthUser]: pass + async def get_all_auth_users_async(self) -> List[AuthUserDTO]: pass @abstractmethod async def get_filtered_auth_users_async(self, criteria: AuthUserSelectCriteria) -> AuthUserFilteredResultDTO: pass @abstractmethod - async def get_auth_user_by_email_async(self, email: str) -> AuthUser: pass + async def get_auth_user_by_email_async(self, email: str) -> AuthUserDTO: pass @abstractmethod - async def find_auth_user_by_email_async(self, email: str) -> AuthUser: pass + async def find_auth_user_by_email_async(self, email: str) -> AuthUserDTO: pass @abstractmethod async def add_auth_user_async(self, user_dto: AuthUserDTO) -> int: pass diff --git a/src/bot_api/api_module.py b/src/bot_api/api_module.py index ff1be35b..6474a3e5 100644 --- a/src/bot_api/api_module.py +++ b/src/bot_api/api_module.py @@ -10,7 +10,7 @@ from flask import Flask from bot_api.abc.auth_service_abc import AuthServiceABC from bot_api.api import Api from bot_api.api_thread import ApiThread -from bot_api.controller.api_controller import ApiController +from bot_api.controller.gui_controller import GuiController from bot_api.controller.auth_controller import AuthController from bot_api.service.auth_service import AuthService from bot_core.abc.module_abc import ModuleABC @@ -38,4 +38,4 @@ class ApiModule(ModuleABC): services.add_transient(AuthServiceABC, AuthService) services.add_transient(AuthController) - services.add_transient(ApiController) + services.add_transient(GuiController) diff --git a/src/bot_api/config/apisettings.edrafts-lapi.json b/src/bot_api/config/apisettings.edrafts-lapi.json index fb7c257c..d71694b2 100644 --- a/src/bot_api/config/apisettings.edrafts-lapi.json +++ b/src/bot_api/config/apisettings.edrafts-lapi.json @@ -7,7 +7,7 @@ "Authentication": { "SecretKey": "F3b5LDz+#Jvzg=W!@gsa%xsF", "Issuer": "http://localhost:5000", - "Audience": "http://localhost:5000", + "Audience": "http://localhost:4200", "TokenExpireTime": 1, "RefreshTokenExpireTime": 7 }, diff --git a/src/bot_api/controller/auth_controller.py b/src/bot_api/controller/auth_controller.py index adb6f3a2..c5e10d1c 100644 --- a/src/bot_api/controller/auth_controller.py +++ b/src/bot_api/controller/auth_controller.py @@ -40,21 +40,25 @@ class AuthController: @Route.get(f'{BasePath}/users') async def get_all_users(self) -> Response: - return jsonify(await self._auth_service.get_all_auth_users_async()) + result = await self._auth_service.get_all_auth_users_async() + return jsonify(result.select(lambda x: x.to_dict())) @Route.post(f'{BasePath}/users/get/filtered') async def get_filtered_users(self) -> Response: dto: AuthUserSelectCriteria = JSONProcessor.process(AuthUserSelectCriteria, request.get_json(force=True, silent=True)) result = await self._auth_service.get_filtered_auth_users_async(dto) + result.result = result.result.select(lambda x: x.to_dict()) return jsonify(result.to_dict()) @Route.get(f'{BasePath}/users/get/') async def get_user_from_email(self, email: str) -> Response: - return jsonify(await self._auth_service.get_auth_user_by_email_async(email)) + result = await self._auth_service.get_auth_user_by_email_async(email) + return jsonify(result.to_dict()) @Route.get(f'{BasePath}/users/find/') async def find_user_from_email(self, email: str) -> Response: - return jsonify(await self._auth_service.find_auth_user_by_email_async(email)) + result = await self._auth_service.find_auth_user_by_email_async(email) + return jsonify(result.to_dict()) @Route.post(f'{BasePath}/register') async def register(self): diff --git a/src/bot_api/controller/api_controller.py b/src/bot_api/controller/gui_controller.py similarity index 92% rename from src/bot_api/controller/api_controller.py rename to src/bot_api/controller/gui_controller.py index 1b7ae669..a2fa883a 100644 --- a/src/bot_api/controller/api_controller.py +++ b/src/bot_api/controller/gui_controller.py @@ -12,7 +12,8 @@ from bot_api.model.version_dto import VersionDTO from bot_api.route.route import Route -class ApiController: +class GuiController: + BasePath = f'/api/gui' def __init__( self, @@ -32,13 +33,13 @@ class ApiController: self._mail_settings = mail_settings self._mailer = mailer - @Route.route('/api/api-version') + @Route.get(f'{BasePath}/api-version') async def api_version(self): import bot_api version = bot_api.version_info return VersionDTO(version.major, version.minor, version.micro).to_dict() - @Route.route('/api/settings') + @Route.get(f'{BasePath}/settings') async def settings(self): # TODO: Authentication import bot_api @@ -59,7 +60,7 @@ class ApiController: self._mail_settings.user_name, ).to_dict() - @Route.route('/api/send-test-mail/') + @Route.get(f'{BasePath}/send-test-mail/') async def send_test_mail(self, email: str): # TODO: Authentication mail = EMail() diff --git a/src/bot_api/filter/auth_user_select_criteria.py b/src/bot_api/filter/auth_user_select_criteria.py index 497f5eaa..8ac7784e 100644 --- a/src/bot_api/filter/auth_user_select_criteria.py +++ b/src/bot_api/filter/auth_user_select_criteria.py @@ -13,7 +13,7 @@ class AuthUserSelectCriteria(SelectCriteriaABC): first_name: str, last_name: str, email: str, - auth_role=0 + auth_role: int ): SelectCriteriaABC.__init__(self, page_index, page_size, sort_direction, sort_column) diff --git a/src/bot_api/model/auth_user_dto.py b/src/bot_api/model/auth_user_dto.py index 19923890..4fb53394 100644 --- a/src/bot_api/model/auth_user_dto.py +++ b/src/bot_api/model/auth_user_dto.py @@ -1,8 +1,5 @@ -import traceback from typing import Optional -from cpl_core.console import Console - from bot_api.abc.dto_abc import DtoABC from bot_data.model.auth_role_enum import AuthRoleEnum @@ -14,7 +11,7 @@ class AuthUserDTO(DtoABC): id: int, first_name: str, last_name: str, - e_mail: str, + email: str, password: str, confirmation_id: Optional[str], auth_role: AuthRoleEnum, @@ -24,7 +21,7 @@ class AuthUserDTO(DtoABC): self._id = id self._first_name = first_name self._last_name = last_name - self._email = e_mail + self._email = email self._password = password self._is_confirmed = confirmation_id is None self._auth_role = auth_role @@ -75,11 +72,11 @@ class AuthUserDTO(DtoABC): @property def auth_role(self) -> AuthRoleEnum: - return self.auth_role + return self._auth_role @auth_role.setter def auth_role(self, value: AuthRoleEnum): - self.auth_role = value + self._auth_role = value def from_dict(self, values: dict): self._id = values['id'] @@ -98,5 +95,5 @@ class AuthUserDTO(DtoABC): 'email': self._email, 'password': self._password, 'isConfirmed': self._is_confirmed, - 'authRole': self._auth_role, + 'authRole': self._auth_role.value, } diff --git a/src/bot_api/service/auth_service.py b/src/bot_api/service/auth_service.py index 61b5cca6..07e75028 100644 --- a/src/bot_api/service/auth_service.py +++ b/src/bot_api/service/auth_service.py @@ -65,24 +65,35 @@ class AuthService(AuthServiceABC): @staticmethod def _is_email_valid(email: str) -> bool: - regex = '^[a-z0-9]+[\\._]?[a-z0-9]+[@]\\w+[.]\\w{2,3}$' - return bool(re.search(regex, email)) + if re.match(re.compile(r'^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$'), email) is not None: + return True + + return False def _generate_token(self, user: AuthUser) -> str: token = jwt.encode( payload={ 'user_id': user.id, 'email': user.email, - 'role': user.auth_role, + 'role': user.auth_role.value, 'exp': datetime.now(tz=timezone.utc) + timedelta(days=self._auth_settings.token_expire_time), 'iss': self._auth_settings.issuer, 'aud': self._auth_settings.audience }, - key=self._auth_settings.secret_key, + key=self._auth_settings.secret_key ) return token + def _decode_token(self, token: str) -> dict: + return jwt.decode( + token, + key=self._auth_settings.secret_key, + issuer=self._auth_settings.issuer, + audience=self._auth_settings.audience, + algorithms=['HS256'] + ) + def _create_and_save_refresh_token(self, user: AuthUser) -> str: token = str(uuid.uuid4()) user.refresh_token = token @@ -113,7 +124,7 @@ class AuthService(AuthServiceABC): mail.body = self._t.transform('api.auth.forgot_password.message').format(url, user.forgot_password_id) self._mailer.send_mail(mail) - async def get_all_auth_users_async(self) -> List[AuthUser]: + async def get_all_auth_users_async(self) -> List[AuthUserDTO]: result = self._auth_users.get_all_auth_users() \ .select(lambda x: AUT.to_dto(x)) return List(AuthUserDTO, result) @@ -127,10 +138,9 @@ class AuthService(AuthServiceABC): users.total_count ) - async def get_auth_user_by_email_async(self, email: str) -> AuthUser: + async def get_auth_user_by_email_async(self, email: str) -> AuthUserDTO: try: - user = self._auth_users.get_auth_user_by_email(email) - return user + return AUT.to_dto(self._auth_users.get_auth_user_by_email(email)) except Exception as e: self._logger.error(__name__, f'AuthUser not found', e) raise ServiceException(ServiceErrorCode.InvalidData, f'User not found {email}') @@ -139,7 +149,7 @@ class AuthService(AuthServiceABC): user = self._auth_users.find_auth_user_by_email(email) return AUT.to_dto(user) if user is not None else None - async def add_auth_user_async(self, user_dto: AuthUserDTO): + async def add_auth_user_async(self, user_dto: AuthUser): db_user = self._auth_users.find_auth_user_by_email(user_dto.email) if db_user is not None: raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists') @@ -151,7 +161,7 @@ class AuthService(AuthServiceABC): try: user.confirmation_id = uuid.uuid4() - self._auth_users.update_auth_user(user) + self._auth_users.add_auth_user(user) self._send_confirmation_id_to_user(user) self._db.save_changes() self._logger.info(__name__, f'Added auth user with E-Mail: {user_dto.email}') @@ -273,7 +283,7 @@ class AuthService(AuthServiceABC): self._logger.error(__name__, f'Cannot delete user', e) raise ServiceException(ServiceErrorCode.UnableToDelete, f'Cannot delete user by mail {email}') - async def delete_auth_user_async(self, user_dto: AuthUserDTO): + async def delete_auth_user_async(self, user_dto: AuthUser): try: self._auth_users.delete_auth_user(AUT.to_db(user_dto)) self._db.save_changes() @@ -281,7 +291,7 @@ class AuthService(AuthServiceABC): self._logger.error(__name__, f'Cannot delete user', e) raise ServiceException(ServiceErrorCode.UnableToDelete, f'Cannot delete user by mail {user_dto.email}') - async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: + async def login_async(self, user_dto: AuthUser) -> TokenDTO: if user_dto is None: raise ServiceException(ServiceErrorCode.InvalidData, 'User not set') @@ -305,12 +315,12 @@ class AuthService(AuthServiceABC): if token_dto is None: raise ServiceException(ServiceErrorCode.InvalidData, f'Token not set') - token = jwt.decode(token_dto.token, key=self._auth_settings.secret_key) + token = self._decode_token(token_dto.token) if token is None or 'email' not in token: raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid') try: - user = self._auth_users.get_auth_user_by_email(token) + user = self._auth_users.get_auth_user_by_email(token['email']) if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired') @@ -323,9 +333,9 @@ class AuthService(AuthServiceABC): if token_dto is None or token_dto.token is None or token_dto.refresh_token is None: raise ServiceException(ServiceErrorCode.InvalidData, 'Token not set') - token = jwt.decode(token_dto.token, key=self._auth_settings.secret_key) + token = self._decode_token(token_dto.token) try: - user = self._auth_users.get_auth_user_by_email(token) + user = self._auth_users.get_auth_user_by_email(token['email']) if user is None or user.refresh_token != token_dto.refresh_token or user.refresh_token_expire_time <= datetime.now(): raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired') diff --git a/src/bot_api/transformer/auth_user_transformer.py b/src/bot_api/transformer/auth_user_transformer.py index 50258b01..b324445b 100644 --- a/src/bot_api/transformer/auth_user_transformer.py +++ b/src/bot_api/transformer/auth_user_transformer.py @@ -1,12 +1,15 @@ +from datetime import datetime, timezone + from bot_api.abc.auth_user_transformer_abc import AuthUserTransformerABC from bot_api.model.auth_user_dto import AuthUserDTO +from bot_data.model.auth_role_enum import AuthRoleEnum from bot_data.model.auth_user import AuthUser class AuthUserTransformer(AuthUserTransformerABC): @staticmethod - def to_db(dto: AuthUserDTO) -> AuthUser: + def to_db(dto: AuthUser) -> AuthUser: return AuthUser( dto.first_name, dto.last_name, @@ -15,9 +18,9 @@ class AuthUserTransformer(AuthUserTransformerABC): None, None, None, - None, - dto.auth_role, - id=dto.id + datetime.now(tz=timezone.utc), + AuthRoleEnum.normal if dto.auth_role is None else dto.auth_role, + id=0 if dto.id is None else dto.id ) @staticmethod @@ -28,6 +31,6 @@ class AuthUserTransformer(AuthUserTransformerABC): db.last_name, db.email, db.password, - db.confirmation_id is None, + db.confirmation_id, db.auth_role ) diff --git a/src/bot_data/migration/api_migration.py b/src/bot_data/migration/api_migration.py index 61f9d156..80754c88 100644 --- a/src/bot_data/migration/api_migration.py +++ b/src/bot_data/migration/api_migration.py @@ -18,18 +18,19 @@ class ApiMigration(MigrationABC): self._cursor.execute( str(f""" CREATE TABLE IF NOT EXISTS `AuthUsers` ( - `Id` bigint NOT NULL, - `FirstName` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, - `LastName` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, - `EMail` varchar(255) DEFAULT NULL, - `Password` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, - `RefreshToken` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, - `ConfirmationId` varchar(255) DEFAULT NULL, - `ForgotPasswordId` varchar(255) DEFAULT NULL, - `RefreshTokenExpiryTime` datetime(6) NOT NULL, - `AuthRole` int NOT NULL DEFAULT '0', - `CreatedOn` datetime(6) NOT NULL, - `LastModifiedOn` datetime(6) NOT NULL + `Id` BIGINT NOT NULL AUTO_INCREMENT, + `FirstName` VARCHAR(255), + `LastName` VARCHAR(255), + `EMail` VARCHAR(255), + `Password` VARCHAR(255), + `RefreshToken` VARCHAR(255), + `ConfirmationId` VARCHAR(255) DEFAULT NULL, + `ForgotPasswordId` VARCHAR(255) DEFAULT NULL, + `RefreshTokenExpiryTime` DATETIME(6) NOT NULL, + `AuthRole` INT NOT NULL DEFAULT '0', + `CreatedOn` DATETIME(6) NOT NULL, + `LastModifiedOn` DATETIME(6) NOT NULL, + PRIMARY KEY(`Id`) ) """) ) diff --git a/src/bot_data/model/auth_role_enum.py b/src/bot_data/model/auth_role_enum.py index 8b032325..e1526136 100644 --- a/src/bot_data/model/auth_role_enum.py +++ b/src/bot_data/model/auth_role_enum.py @@ -3,5 +3,5 @@ from enum import Enum class AuthRoleEnum(Enum): - Normal = 0 - Admin = 1 + normal = 0 + admin = 1 diff --git a/src/bot_data/model/auth_user.py b/src/bot_data/model/auth_user.py index 57cdde0a..26ba09a8 100644 --- a/src/bot_data/model/auth_user.py +++ b/src/bot_data/model/auth_user.py @@ -17,7 +17,7 @@ class AuthUser(TableABC): refresh_token: Optional[str], confirmation_id: Optional[str], forgot_password_id: Optional[str], - refresh_token_expire_time: Optional[datetime], + refresh_token_expire_time: datetime, auth_role: AuthRoleEnum, created_at: datetime = None, modified_at: datetime = None, @@ -100,11 +100,11 @@ class AuthUser(TableABC): self._forgot_password_id = value @property - def refresh_token_expire_time(self) -> Optional[datetime]: + def refresh_token_expire_time(self) -> datetime: return self._refresh_token_expire_time @refresh_token_expire_time.setter - def refresh_token_expire_time(self, value: Optional[datetime]): + def refresh_token_expire_time(self, value: datetime): self._refresh_token_expire_time = value @property @@ -143,7 +143,7 @@ class AuthUser(TableABC): """) @staticmethod - def get_select_by_forgot_password_i_string(id: str) -> str: + def get_select_by_forgot_password_id_string(id: str) -> str: return str(f""" SELECT * FROM `AuthUsers` WHERE `ForgotPasswordId` = '{id}'; @@ -172,8 +172,8 @@ class AuthUser(TableABC): '{self._email}', '{self._password}', '{self._refresh_token}', - '{self._confirmation_id}', - '{self._forgot_password_id}', + '{"NULL" if self._confirmation_id is None else self._confirmation_id}', + '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}', '{self._refresh_token_expire_time}', {self._auth_role_id.value}, '{self._created_at}', @@ -190,11 +190,11 @@ class AuthUser(TableABC): `EMail` = '{self._email}', `Password` = '{self._password}', `RefreshToken` = '{self._refresh_token}', - `ConfirmationId` = '{self._confirmation_id}', - `ForgotPasswordId` = '{self._forgot_password_id}', + `ConfirmationId` = '{"NULL" if self._confirmation_id is None else self._confirmation_id}', + `ForgotPasswordId` = '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}', `RefreshTokenExpiryTime` = '{self._refresh_token_expire_time}', - `AutoRole` = {self._auth_role_id.value}, - `LastModifiedAt` = '{self._modified_at}' + `AuthRole` = {self._auth_role_id.value}, + `LastModifiedOn` = '{self._modified_at}' WHERE `AuthUsers`.`Id` = {self._auth_user_id}; """) diff --git a/src/bot_data/service/auth_user_repository_service.py b/src/bot_data/service/auth_user_repository_service.py index 2096113e..4918b269 100644 --- a/src/bot_data/service/auth_user_repository_service.py +++ b/src/bot_data/service/auth_user_repository_service.py @@ -20,18 +20,24 @@ class AuthUserRepositoryService(AuthUserRepositoryABC): AuthUserRepositoryABC.__init__(self) @staticmethod - def _user_from_result(result: tuple) -> AuthUser: + def _get_value_from_result(value: any) -> Optional[any]: + if isinstance(value, str) and 'NULL' in value: + return None + + return value + + def _user_from_result(self, result: tuple) -> AuthUser: return AuthUser( - result[1], - result[2], - result[3], - result[4], - result[5], - result[6], - result[7], - result[8], - AuthRoleEnum(result[9]), - id=result[0] + self._get_value_from_result(result[1]), + self._get_value_from_result(result[2]), + self._get_value_from_result(result[3]), + self._get_value_from_result(result[4]), + self._get_value_from_result(result[5]), + self._get_value_from_result(result[6]), + self._get_value_from_result(result[7]), + self._get_value_from_result(result[8]), + AuthRoleEnum(self._get_value_from_result(result[9])), + id=self._get_value_from_result(result[0]) ) def get_all_auth_users(self) -> List[AuthUser]: @@ -47,7 +53,6 @@ class AuthUserRepositoryService(AuthUserRepositoryABC): def get_filtered_auth_users(self, criteria: AuthUserSelectCriteria) -> FilteredResult: users = self.get_all_auth_users() self._logger.trace(__name__, f'Send SQL command: {AuthUser.get_select_all_string()}') - query = users if criteria.first_name is not None and criteria.first_name != '': @@ -70,9 +75,9 @@ class AuthUserRepositoryService(AuthUserRepositoryABC): else: query = query.order_by(lambda x: getattr(x, criteria.sort_column)) - skip = criteria.page_size * criteria.page_index result = FilteredResult() result.total_count = query.count() + skip = criteria.page_size * criteria.page_index result.result = query.skip(skip).take(criteria.page_size) return result @@ -93,8 +98,8 @@ class AuthUserRepositoryService(AuthUserRepositoryABC): return self._user_from_result(result) def find_auth_user_by_confirmation_id(self, id: str) -> Optional[AuthUser]: - self._logger.trace(__name__, f'Send SQL command: {AuthUser.get_select_by_email_string(id)}') - result = self._context.select(AuthUser.get_select_by_email_string(id)) + self._logger.trace(__name__, f'Send SQL command: {AuthUser.get_select_by_confirmation_id_string(id)}') + result = self._context.select(AuthUser.get_select_by_confirmation_id_string(id)) if result is None or len(result) == 0: return None @@ -103,8 +108,8 @@ class AuthUserRepositoryService(AuthUserRepositoryABC): return self._user_from_result(result) def find_auth_user_by_forgot_password_id(self, id: str) -> Optional[AuthUser]: - self._logger.trace(__name__, f'Send SQL command: {AuthUser.get_select_by_email_string(id)}') - result = self._context.select(AuthUser.get_select_by_email_string(id)) + self._logger.trace(__name__, f'Send SQL command: {AuthUser.get_select_by_forgot_password_id_string(id)}') + result = self._context.select(AuthUser.get_select_by_forgot_password_id_string(id)) if result is None or len(result) == 0: return None