Added flask support #70 #75 #71

Merged
edraft merged 107 commits from #70 into 0.3 2022-11-05 13:55:42 +01:00
7 changed files with 114 additions and 9 deletions
Showing only changes of commit 3fe8e1503c - Show all commits

View File

@ -11,6 +11,7 @@ from bot.startup_discord_extension import StartupDiscordExtension
from bot.startup_migration_extension import StartupMigrationExtension
from bot.startup_module_extension import StartupModuleExtension
from bot.startup_settings_extension import StartupSettingsExtension
from bot_api.app_api_extension import AppApiExtension
from modules.boot_log.boot_log_extension import BootLogExtension
from modules.database.database_extension import DatabaseExtension
@ -29,6 +30,7 @@ class Program:
.use_extension(StartupMigrationExtension) \
.use_extension(BootLogExtension) \
.use_extension(DatabaseExtension) \
.use_extension(AppApiExtension) \
.use_startup(Startup)
self.app: Application = await app_builder.build_async()
await self.app.run_async()

View File

@ -9,6 +9,7 @@ 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):
@ -16,6 +17,12 @@ class AuthServiceABC(ABC):
@abstractmethod
def __init__(self): pass
@abstractmethod
def generate_token(self, user: AuthUser) -> str: pass
@abstractmethod
def decode_token(self, token: str) -> dict: pass
@abstractmethod
async def get_all_auth_users_async(self) -> List[AuthUserDTO]: pass
@ -43,6 +50,9 @@ class AuthServiceABC(ABC):
@abstractmethod
async def delete_auth_user_async(self, user_dto: AuthUserDTO): pass
@abstractmethod
async def verify_login(self, token_str: str) -> bool: pass
@abstractmethod
async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO: pass

View File

@ -0,0 +1,26 @@
from cpl_core.application import ApplicationExtensionABC
from cpl_core.configuration import ConfigurationABC
from cpl_core.dependency_injection import ServiceProviderABC
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.route.route import Route
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
class AppApiExtension(ApplicationExtensionABC):
def __init__(self):
ApplicationExtensionABC.__init__(self)
async def run(self, config: ConfigurationABC, services: ServiceProviderABC):
feature_flags: FeatureFlagsSettings = config.get_configuration(FeatureFlagsSettings)
if not feature_flags.get_flag(FeatureFlagsEnum.api_module):
return
auth_settings: AuthenticationSettings = config.get_configuration(AuthenticationSettings)
auth_users: AuthUserRepositoryABC = services.get_service(AuthUserRepositoryABC)
auth: AuthServiceABC = services.get_service(AuthServiceABC)
Route.init_authorize(auth_users, auth)

View File

@ -39,11 +39,13 @@ class AuthController:
self._auth_service = auth_service
@Route.get(f'{BasePath}/users')
@Route.authorize
async def get_all_users(self) -> Response:
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')
@Route.authorize
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)
@ -51,11 +53,13 @@ class AuthController:
return jsonify(result.to_dict())
@Route.get(f'{BasePath}/users/get/<email>')
@Route.authorize
async def get_user_from_email(self, email: str) -> Response:
result = await self._auth_service.get_auth_user_by_email_async(email)
return jsonify(result.to_dict())
@Route.get(f'{BasePath}/users/find/<email>')
edraft marked this conversation as resolved
Review

Was passiert, wenn eine andere E-Mail-Adresse angegeben wird?
Wenn eine positive Antwort gegeben wird: Ist es gewollt, dass ein User eine andere E-Mail-Adresse aufrufen kann?
Wenn ja: Soll ein User eine E-Mail-Adresse abfragen können, mit dem dieser nichts zu tun hat?

Was passiert, wenn eine andere E-Mail-Adresse angegeben wird? Wenn eine positive Antwort gegeben wird: Ist es gewollt, dass ein User eine andere E-Mail-Adresse aufrufen kann? Wenn ja: Soll ein User eine E-Mail-Adresse abfragen können, mit dem dieser nichts zu tun hat?
Review

was spricht dagegen?
So kannst du Profile von anderen usern sehen, kannst damit ja nichts ans pw kommen.

was spricht dagegen? So kannst du Profile von anderen usern sehen, kannst damit ja nichts ans pw kommen.
Review

Naja, so könnte sich jemand registrieren und dann somit eine Liste von gültigen E-Mails erstellen für Spammers.

Naja, so könnte sich jemand registrieren und dann somit eine Liste von gültigen E-Mails erstellen für Spammers.
Review

Wenn der User nur Profile von anderen Usern aufrufen kann, mit dem er zu tun hat, dann wäre es besser.

Wenn der User nur Profile von anderen Usern aufrufen kann, mit dem er zu tun hat, dann wäre es besser.
@Route.authorize
async def find_user_from_email(self, email: str) -> Response:
result = await self._auth_service.find_auth_user_by_email_async(email)
return jsonify(result.to_dict())
@ -83,36 +87,42 @@ class AuthController:
return '', 200
@Route.post(f'{BasePath}/update-user')
@Route.authorize
async def update_user(self):
dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.update_user_async(dto)
return '', 200
@Route.post(f'{BasePath}/update-user-as-admin')
@Route.authorize
async def update_user_as_admin(self):
dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.update_user_async(dto)
return '', 200
@Route.post(f'{BasePath}/refresh')
@Route.authorize
async def refresh(self) -> Response:
dto: TokenDTO = JSONProcessor.process(TokenDTO, request.get_json(force=True, silent=True))
result = await self._auth_service.refresh_async(dto)
return jsonify(result.to_dict())
@Route.post(f'{BasePath}/revoke')
@Route.authorize
async def revoke(self):
dto: TokenDTO = JSONProcessor.process(TokenDTO, request.get_json(force=True, silent=True))
await self._auth_service.revoke_async(dto)
return '', 200
@Route.post(f'{BasePath}/delete-user')
@Route.authorize
async def delete_user(self):
edraft marked this conversation as resolved
Review

Was passiert, wenn ein User eine JSON sendet mit der E-Mail-Adresse eines anderen Users?
Also versuchen die Einstellungen eines anderen Users überscheiben.

Was passiert, wenn ein User eine JSON sendet mit der E-Mail-Adresse eines anderen Users? Also versuchen die Einstellungen eines anderen Users überscheiben.
Review

Er muss das Passwort des Users kennen

Er muss das Passwort des Users kennen
dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.delete_auth_user_async(dto)
return '', 200
@Route.post(f'{BasePath}/delete-user-by-mail/<email>')
@Route.authorize
async def delete_user_by_mail(self, email: str):
await self._auth_service.delete_auth_user_by_email_async(email)
return '', 200

View File

@ -40,6 +40,7 @@ class GuiController:
return VersionDTO(version.major, version.minor, version.micro).to_dict()
@Route.get(f'{BasePath}/settings')
@Route.authorize
async def settings(self):
# TODO: Authentication
import bot_api
@ -61,6 +62,7 @@ class GuiController:
).to_dict()
@Route.get(f'{BasePath}/send-test-mail/<email>')
@Route.authorize
async def send_test_mail(self, email: str):
# TODO: Authentication
mail = EMail()

View File

@ -1,9 +1,48 @@
from functools import wraps
from typing import Optional
from flask import request
from flask_cors import cross_origin
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.exception.service_error_code_enum import ServiceErrorCode
from bot_api.exception.service_exception import ServiceException
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
class Route:
registered_routes = {}
_auth_users: Optional[AuthUserRepositoryABC] = None
_auth: Optional[AuthServiceABC] = None
@classmethod
def init_authorize(cls, auth_users: AuthUserRepositoryABC, auth: AuthServiceABC):
cls._auth_users = auth_users
cls._auth = auth
@classmethod
def authorize(cls, f):
@wraps(f)
async def decorator(*args, **kwargs):
token = None
edraft marked this conversation as resolved
Review

Vorschlag: Die HTTP-Response-Status-Codes in einem Enum packen, damit man schneller erkennen kann, um was die Response handelt.

Vorschlag: Die HTTP-Response-Status-Codes in einem Enum packen, damit man schneller erkennen kann, um was die Response handelt.
Review

Kann man später mal nachbessern aber die API ist jetzt noch übersichtlich genug

Kann man später mal nachbessern aber die API ist jetzt noch übersichtlich genug
if 'Authorization' in request.headers:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]
if not token:
raise ServiceException(ServiceErrorCode.InvalidData, f'Token not set')
if cls._auth_users is None or cls._auth is None:
raise ServiceException(ServiceErrorCode.InvalidDependencies, f'Authorize is not initialized')
if not cls._auth.verify_login(token):
raise ServiceException(ServiceErrorCode.InvalidUser, f'Token expired')
return await f(*args, **kwargs)
return decorator
@classmethod
def route(cls, path=None, **kwargs):
# simple decorator for class based views

View File

@ -70,7 +70,7 @@ class AuthService(AuthServiceABC):
return False
def _generate_token(self, user: AuthUser) -> str:
def generate_token(self, user: AuthUser) -> str:
token = jwt.encode(
payload={
'user_id': user.id,
@ -85,7 +85,7 @@ class AuthService(AuthServiceABC):
return token
def _decode_token(self, token: str) -> dict:
def decode_token(self, token: str) -> dict:
return jwt.decode(
token,
key=self._auth_settings.secret_key,
@ -292,6 +292,21 @@ 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}')
def verify_login(self, token_str: str) -> bool:
try:
token = self.decode_token(token_str)
if token is None or 'email' not in token:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid')
user = self._auth_users.find_auth_user_by_email(token['email'])
if user is None:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token expired')
except Exception as e:
self._logger.error(__name__, f'Refreshing token failed', e)
return False
return True
async def login_async(self, user_dto: AuthUser) -> TokenDTO:
if user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, 'User not set')
@ -304,7 +319,7 @@ class AuthService(AuthServiceABC):
if db_user.password != user_dto.password:
raise ServiceException(ServiceErrorCode.InvalidUser, 'Wrong password')
token = self._generate_token(db_user)
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
@ -316,16 +331,16 @@ class AuthService(AuthServiceABC):
if token_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'Token not set')
token = self._decode_token(token_dto.token)
if token is None or 'email' not in token:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid')
try:
token = self.decode_token(token_dto.token)
if token is None or 'email' not in token:
raise ServiceException(ServiceErrorCode.InvalidData, 'Token invalid')
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')
return TokenDTO(self._generate_token(user), self._create_and_save_refresh_token(user))
return TokenDTO(self.generate_token(user), self._create_and_save_refresh_token(user))
except Exception as e:
self._logger.error(__name__, f'Refreshing token failed', e)
return TokenDTO('', '')
@ -334,8 +349,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 = self._decode_token(token_dto.token)
try:
token = self.decode_token(token_dto.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')