Added flask support #70 #75 #71

Merged
edraft merged 107 commits from #70 into 0.3 2022-11-05 13:55:42 +01:00
16 changed files with 108 additions and 88 deletions
Showing only changes of commit 119f4e9d04 - Show all commits

View File

@ -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

View File

@ -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": [

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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
},

View File

@ -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/<email>')
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/<email>')
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):

View File

@ -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/<email>')
@Route.get(f'{BasePath}/send-test-mail/<email>')
async def send_test_mail(self, email: str):
# TODO: Authentication
mail = EMail()

View File

@ -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)

View File

@ -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,
}

View File

@ -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')

View File

@ -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
)

View File

@ -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`)
)
""")
)

View File

@ -3,5 +3,5 @@ from enum import Enum
class AuthRoleEnum(Enum):
Normal = 0
Admin = 1
normal = 0
admin = 1

View File

@ -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};
""")

View File

@ -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