Added flask support #70 #75 #71

Merged
edraft merged 107 commits from #70 into 0.3 2022-11-05 13:55:42 +01:00
12 changed files with 407 additions and 47 deletions
Showing only changes of commit 1090e502c2 - Show all commits

View File

@ -159,6 +159,16 @@
"subject": "Krümmelmonster Web Interface Test-Mail",
"message": "Dies ist eine Test-Mail vom Krümmelmonster Web Interface\nGesendet von {}-{}"
}
}
},
"auth": {
"confirmation": {
"subject": "E-Mail für {} {} bestätigen",
"message": "Öffne den Link um die E-Mail zu bestätigen:\n{}auth/forgot-password/{}"
},
"forgot_password": {
"subject": "Passwort für {} {} zurücksetzen",
"message": "Öffne den Link um das Passwort zu ändern:\n{}auth/forgot-password/{}"
}
}
}
}

View File

@ -0,0 +1,16 @@
from abc import abstractmethod
from cpl_core.database import TableABC
from bot_api.abc.dto_abc import DtoABC
class AuthUserTransformerABC:
@staticmethod
@abstractmethod
def to_db(dto: DtoABC) -> TableABC: pass
@staticmethod
@abstractmethod
def to_dto(db: TableABC) -> DtoABC: pass

View File

@ -21,6 +21,7 @@
"Flask[async]==2.2.2",
"Flask-Classful==0.14.2",
"Flask-Cors==3.0.10",
"PyJWT[crypto]==2.5.0",
"PyJWT==2.5.0"
],
"DevDependencies": [

View File

@ -7,12 +7,15 @@ from bot_api.abc.dto_abc import DtoABC
class EMailStringDTO(DtoABC):
def __init__(self):
def __init__(self, email: str):
DtoABC.__init__(self)
self._email = email
def from_dict(self, values: dict):
pass
self._email = values['EMail']
def to_dict(self) -> dict:
return {
'EMail': self._email
}

View File

@ -7,12 +7,26 @@ from bot_api.abc.dto_abc import DtoABC
class ResetPasswordDTO(DtoABC):
def __init__(self):
def __init__(self, id: str, password: str):
DtoABC.__init__(self)
self._id = id
self._password = password
@property
def id(self) -> str:
return self._id
@property
def password(self) -> str:
return self._password
def from_dict(self, values: dict):
pass
self._id = values['Id']
self._password = values['Password']
def to_dict(self) -> dict:
return {
'Id': self._id,
'Password': self._password
}

View File

@ -3,16 +3,43 @@ import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
from bot_api.model.auth_user_dto import AuthUserDTO
class UpdateAuthUserDTO(DtoABC):
def __init__(self):
def __init__(
self,
auth_user: AuthUserDTO,
new_auth_user: AuthUserDTO,
change_password=False
):
DtoABC.__init__(self)
self._auth_user = auth_user
self._new_auth_user = new_auth_user
self._change_password = change_password
@property
def auth_user(self) -> AuthUserDTO:
return self._auth_user
@property
def new_auth_user(self) -> AuthUserDTO:
return self._new_auth_user
@property
def change_password(self) -> bool:
return self._change_password
def from_dict(self, values: dict):
pass
self._auth_user = values['AuthUser']
self._new_auth_user = values['NewAuthUser']
self._change_password = False if 'ChangePassword' not in values else values['ChangePassword']
def to_dict(self) -> dict:
return {
'AuthUser': self._auth_user,
'NewAuthUser': self._new_auth_user,
'ChangePassword': self._change_password
}

View File

@ -1,13 +1,25 @@
import hashlib
import re
import uuid
from typing import Optional
from cpl_core.database.context import DatabaseContextABC
from cpl_core.mailing import EMailClientABC, EMail
from cpl_query.extension import List
from cpl_translation import TranslatePipe
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.configuration.frontend_settings import FrontendSettings
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
from bot_api.model.auth_user import AuthUserDTO
from bot_api.model.auth_user_filtered_result_dto import AuthUserFilteredResultDTO
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_api.transformer.auth_user_transformer import AuthUserTransformer as AUT
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.model.auth_user import AuthUser
@ -16,46 +28,235 @@ class AuthService(AuthServiceABC):
def __init__(
self,
logger: ApiLogger,
auth_users: AuthUserRepositoryABC,
db: DatabaseContextABC,
mailer: EMailClientABC,
t: TranslatePipe,
frontend_settings: FrontendSettings,
):
AuthServiceABC.__init__(self)
self._logger = logger
self._auth_users = auth_users
self._db = db
self._mailer = mailer
self._t = t
self._frontend_settings = frontend_settings
@staticmethod
def _get_mail_to_send() -> EMail:
mail = EMail()
mail.add_header('Mime-Version: 1.0')
mail.add_header('Content-Type: text/plain charset=utf-8')
mail.add_header('Content-Transfer-Encoding: quoted-printable')
return mail
@staticmethod
def _hash_sha256(password: str) -> str:
return hashlib.sha256(password.encode('utf-8')).hexdigest()
@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))
def _send_confirmation_id_to_user(self, user: AuthUser):
url = self._frontend_settings.url
if not url.endswith('/'):
url = f'{url}/'
mail = self._get_mail_to_send()
mail.add_receiver(user.email)
mail.subject = self._t.transform('api.auth.confirmation.subject').format(user.first_name, user.last_name)
mail.body = self._t.transform('api.auth.confirmation.message').format(url, user.confirmation_id)
self._mailer.send_mail(mail)
def _send_forgot_password_id_to_user(self, user: AuthUser):
url = self._frontend_settings.url
if not url.endswith('/'):
url = f'{url}/'
mail = self._get_mail_to_send()
mail.add_receiver(user.email)
mail.subject = self._t.transform('api.auth.forgot_password.subject').format(user.first_name, user.last_name)
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]:
pass
result = self._auth_users.get_all_auth_users() \
.select(lambda x: AUT.to_dto(x))
return List(AuthUserDTO, result)
async def get_filtered_auth_users_async(self, criteria: AuthUserSelectCriteria) -> AuthUserFilteredResultDTO:
pass
users = self._auth_users.get_filtered_auth_users(criteria)
result = users.result.select(lambda x: AUT.to_dto(x))
return AuthUserFilteredResultDTO(
List(AuthUserDTO, result),
users.total_count
)
async def get_auth_user_by_email_async(self, email: str) -> AuthUser:
pass
try:
user = self._auth_users.get_auth_user_by_email(email)
return user
except Exception as e:
self._logger.error(__name__, f'AuthUser not found', e)
raise Exception(f'User not found {email}')
async def find_auth_user_by_email_async(self, email: str) -> AuthUser:
pass
async def find_auth_user_by_email_async(self, email: str) -> Optional[AuthUser]:
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) -> AuthUser:
pass
async def confirm_email_async(self, id: str) -> AuthUser:
pass
async def confirm_email_async(self, id: str) -> bool:
user = self._auth_users.find_auth_user_by_confirmation_id(id)
if user is None:
return False
user.confirmation_id = None
self._auth_users.update_auth_user(user)
self._db.save_changes()
return True
async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO:
pass
async def forgot_password_async(self, email: str) -> AuthUser:
pass
async def forgot_password_async(self, email: str):
user = self._auth_users.find_auth_user_by_email(email)
if user is None:
return
async def confirm_forgot_password_async(self, id: str) -> AuthUser:
pass
user.forgot_password_id = uuid.uuid4()
self._auth_users.update_auth_user(user)
self._send_forgot_password_id_to_user(user)
self._db.save_changes()
async def reset_password_async(self, rp_dto: ResetPasswordDTO) -> AuthUser:
pass
async def confirm_forgot_password_async(self, id: str) -> EMailStringDTO:
user = self._auth_users.find_auth_user_by_forgot_password_id(id)
return EMailStringDTO(user.email)
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO) -> AuthUser:
pass
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:
pass
async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO) -> AuthUser:
pass
if user.confirmation_id is not None:
pass
if user.password is None or rp_dto.password == '':
pass
user.password = self._hash_sha256(rp_dto.password)
self._db.save_changes()
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None:
raise Exception(f'User is empty')
if update_user_dto.auth_user is None:
raise Exception(f'Existing user is empty')
if update_user_dto.new_auth_user is None:
raise Exception(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')
user = self._auth_users.find_auth_user_by_email(update_user_dto.auth_user.email)
if user is None:
raise Exception('User not found')
if user.confirmation_id is not None:
raise Exception('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:
user.FirstName = update_user_dto.new_auth_user.first_name
# update last name
if update_user_dto.new_auth_user.last_name is not None and update_user_dto.new_auth_user.last_name != '' and \
update_user_dto.auth_user.last_name != update_user_dto.new_auth_user.last_name:
user.LastName = update_user_dto.new_auth_user.last_name
# update E-Mail
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')
user.email = update_user_dto.new_auth_user.email
is_existing_password_set = False
is_new_password_set = False
# hash passwords in DTOs
if update_user_dto.auth_user.Password is not None and update_user_dto.auth_user.Password != '':
is_existing_password_set = True
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')
if update_user_dto.new_auth_user.Password is not None and update_user_dto.new_auth_user.Password != '':
is_new_password_set = True
update_user_dto.new_auth_user.Password = self._hash_sha256(update_user_dto.new_auth_user.Password)
# update password
if is_existing_password_set and is_new_password_set and update_user_dto.auth_user.Password != update_user_dto.new_auth_user.Password:
user.Password = update_user_dto.new_auth_user.Password
self._db.save_changes()
async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None:
raise Exception(f'User is empty')
if update_user_dto.auth_user is None:
raise Exception(f'Existing user is empty')
if update_user_dto.new_auth_user is None:
raise Exception(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')
user = self._auth_users.find_auth_user_by_email(update_user_dto.auth_user.email)
if user is None:
raise Exception('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')
# 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:
user.FirstName = update_user_dto.new_auth_user.first_name
# update last name
if update_user_dto.new_auth_user.last_name is not None and update_user_dto.new_auth_user.last_name != '' and update_user_dto.auth_user.last_name != update_user_dto.new_auth_user.last_name:
user.LastName = update_user_dto.new_auth_user.last_name
# update E-Mail
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')
user.EMail = update_user_dto.new_auth_user.email
# update password
if update_user_dto.change_password and update_user_dto.auth_user.password != update_user_dto.new_auth_user.password:
user.Password = self._hash_sha256(update_user_dto.new_auth_user.password)
# update role
if user.auth_role == update_user_dto.auth_user.auth_role and user.auth_role != update_user_dto.new_auth_user.auth_role:
user.auth_role = update_user_dto.new_auth_user.auth_role
self._db.save_changes()
async def refresh_async(self, token_dto: TokenDTO) -> AuthUser:
pass
@ -63,8 +264,19 @@ class AuthService(AuthServiceABC):
async def revoke_async(self, token_dto: TokenDTO) -> AuthUser:
pass
async def delete_auth_user_by_email_async(self, email: str) -> AuthUser:
pass
async def delete_auth_user_by_email_async(self, email: str):
try:
user = self._auth_users.get_auth_user_by_email(email)
self._auth_users.delete_auth_user(user)
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}')
async def delete_auth_user_async(self, user_dto: AuthUserDTO) -> AuthUser:
pass
async def delete_auth_user_async(self, user_dto: AuthUserDTO):
try:
self._auth_users.delete_auth_user(AUT.to_db(user_dto))
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}')

View File

View File

@ -0,0 +1,33 @@
from bot_api.abc.auth_user_transformer_abc import AuthUserTransformerABC
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_data.model.auth_user import AuthUser
class AuthUserTransformer(AuthUserTransformerABC):
@staticmethod
def to_db(dto: AuthUserDTO) -> AuthUser:
return AuthUser(
dto.first_name,
dto.last_name,
dto.email,
dto.password,
None,
None,
None,
None,
dto.auth_role,
id=dto.id
)
@staticmethod
def to_dto(db: AuthUser) -> AuthUserDTO:
return AuthUserDTO(
db.id,
db.first_name,
db.last_name,
db.email,
db.password,
db.confirmation_id is None,
db.auth_role
)

View File

@ -17,19 +17,19 @@ class AuthUserRepositoryABC(ABC):
def get_all_auth_users(self) -> List[AuthUser]: pass
@abstractmethod
def get_filtered_auth_users_async(self, criteria: AuthUserSelectCriteria) -> FilteredResult: pass
def get_filtered_auth_users(self, criteria: AuthUserSelectCriteria) -> FilteredResult: pass
@abstractmethod
def get_auth_user_by_email_async(self, email: str) -> AuthUser: pass
def get_auth_user_by_email(self, email: str) -> AuthUser: pass
@abstractmethod
def find_auth_user_by_email_async(self, email: str) -> Optional[AuthUser]: pass
def find_auth_user_by_email(self, email: str) -> Optional[AuthUser]: pass
@abstractmethod
def find_auth_user_by_confirmation_id_async(self, id: str) -> Optional[AuthUser]: pass
def find_auth_user_by_confirmation_id(self, id: str) -> Optional[AuthUser]: pass
@abstractmethod
def find_auth_user_by_forgot_password_id_async(self, id: str) -> Optional[AuthUser]: pass
def find_auth_user_by_forgot_password_id(self, id: str) -> Optional[AuthUser]: pass
@abstractmethod
def add_auth_user(self, user: AuthUser): pass

View File

@ -14,10 +14,10 @@ class AuthUser(TableABC):
last_name: str,
email: str,
password: str,
refresh_token: str,
confirmation_id: str,
forgot_password_id: str,
refresh_token_expire_time: datetime,
refresh_token: Optional[str],
confirmation_id: Optional[str],
forgot_password_id: Optional[str],
refresh_token_expire_time: Optional[datetime],
auth_role: AuthRoleEnum,
created_at: datetime = None,
modified_at: datetime = None,
@ -47,30 +47,74 @@ class AuthUser(TableABC):
def first_name(self) -> str:
return self._first_name
@first_name.setter
def first_name(self, value: str):
self._first_name = value
@property
def last_name(self) -> str:
return self._last_name
@last_name.setter
def last_name(self, value: str):
self._last_name = value
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, value: str):
self._email = value
@property
def password(self) -> str:
return self._password
@property
def refresh_token(self) -> str:
return self._refresh_token
@password.setter
def password(self, value: str):
self._password = value
@property
def refresh_token_expire_time(self) -> datetime:
def refresh_token(self) -> Optional[str]:
return self._refresh_token
@refresh_token.setter
def refresh_token(self, value: Optional[str]):
self._refresh_token = value
@property
def confirmation_id(self) -> Optional[str]:
return self._confirmation_id
@confirmation_id.setter
def confirmation_id(self, value: Optional[str]):
self._confirmation_id = value
@property
def forgot_password_id(self) -> Optional[str]:
return self._forgot_password_id
@forgot_password_id.setter
def forgot_password_id(self, value: Optional[str]):
self._forgot_password_id = value
@property
def refresh_token_expire_time(self) -> Optional[datetime]:
return self._refresh_token_expire_time
@refresh_token_expire_time.setter
def refresh_token_expire_time(self, value: Optional[datetime]):
self._refresh_token_expire_time = value
@property
def auth_role(self) -> AuthRoleEnum:
return self._auth_role_id
@auth_role.setter
def auth_role(self, value: AuthRoleEnum):
self._auth_role_id = value
@staticmethod
def get_select_all_string() -> str:
return str(f"""

View File

@ -44,7 +44,7 @@ class AuthUserRepositoryService(AuthUserRepositoryABC):
return users
def get_filtered_auth_users_async(self, criteria: AuthUserSelectCriteria) -> FilteredResult:
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()}')
@ -77,12 +77,12 @@ class AuthUserRepositoryService(AuthUserRepositoryABC):
return result
def get_auth_user_by_email_async(self, email: str) -> AuthUser:
def get_auth_user_by_email(self, email: str) -> AuthUser:
self._logger.trace(__name__, f'Send SQL command: {AuthUser.get_select_by_email_string(email)}')
result = self._context.select(AuthUser.get_select_by_email_string(email))[0]
return self._user_from_result(result)
def find_auth_user_by_email_async(self, email: str) -> Optional[AuthUser]:
def find_auth_user_by_email(self, email: str) -> Optional[AuthUser]:
self._logger.trace(__name__, f'Send SQL command: {AuthUser.get_select_by_email_string(email)}')
result = self._context.select(AuthUser.get_select_by_email_string(email))
if result is None or len(result) == 0:
@ -92,7 +92,7 @@ class AuthUserRepositoryService(AuthUserRepositoryABC):
return self._user_from_result(result)
def find_auth_user_by_confirmation_id_async(self, id: str) -> Optional[AuthUser]:
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))
if result is None or len(result) == 0:
@ -102,7 +102,7 @@ class AuthUserRepositoryService(AuthUserRepositoryABC):
return self._user_from_result(result)
def find_auth_user_by_forgot_password_id_async(self, id: str) -> Optional[AuthUser]:
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))
if result is None or len(result) == 0: