Moved folders #405

This commit is contained in:
2023-10-13 17:10:00 +02:00
parent eb32bec43c
commit f435d3dd48
807 changed files with 3801 additions and 1297 deletions

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.abc"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,120 @@
from abc import ABC, abstractmethod
from typing import Optional
from cpl_query.extension import List
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
from bot_api.model.auth_user_dto 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.o_auth_dto import OAuthDTO
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):
@abstractmethod
def __init__(self):
pass
@abstractmethod
def generate_token(self, user: AuthUser) -> str:
pass
@abstractmethod
def decode_token(self, token: str) -> dict:
pass
@abstractmethod
def get_decoded_token_from_request(self) -> dict:
pass
@abstractmethod
def find_decoded_token_from_request(self) -> Optional[dict]:
pass
@abstractmethod
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, with_password: bool = False
) -> AuthUserDTO:
pass
@abstractmethod
async def find_auth_user_by_email_async(self, email: str) -> AuthUserDTO:
pass
@abstractmethod
def add_auth_user(self, user_dto: AuthUserDTO):
pass
@abstractmethod
async def add_auth_user_by_oauth_async(self, dto: OAuthDTO):
pass
@abstractmethod
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
pass
@abstractmethod
async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO):
pass
@abstractmethod
async def delete_auth_user_by_email_async(self, email: str):
pass
@abstractmethod
async def delete_auth_user_async(self, user_dto: AuthUserDTO):
pass
@abstractmethod
async def verify_login(self, token_str: str) -> bool:
pass
@abstractmethod
def verify_api_key(self, api_key: str) -> bool:
pass
@abstractmethod
async def login_async(self, user_dto: AuthUserDTO) -> TokenDTO:
pass
@abstractmethod
async def login_discord_async(self, oauth_dto: AuthUserDTO, dc_id: int) -> TokenDTO:
pass
@abstractmethod
async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO:
pass
@abstractmethod
async def revoke_async(self, token_dto: TokenDTO):
pass
@abstractmethod
async def confirm_email_async(self, id: str) -> bool:
pass
@abstractmethod
async def forgot_password_async(self, email: str):
pass
@abstractmethod
async def confirm_forgot_password_async(self, id: str) -> EMailStringDTO:
pass
@abstractmethod
async def reset_password_async(self, rp_dto: ResetPasswordDTO):
pass

View File

@@ -0,0 +1,15 @@
from abc import ABC, abstractmethod
class DtoABC(ABC):
@abstractmethod
def __init__(self):
pass
@abstractmethod
def from_dict(self, values: dict):
pass
@abstractmethod
def to_dict(self) -> dict:
pass

View File

@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
class SelectCriteriaABC(ABC):
@abstractmethod
def __init__(
self, page_index: int, page_size: int, sort_direction: str, sort_column: str
):
self.page_index = page_index
self.page_size = page_size
self.sort_direction = sort_direction
self.sort_column = sort_column

View File

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

180
bot/src/bot_api/api.py Normal file
View File

@@ -0,0 +1,180 @@
import socket
import sys
import textwrap
import uuid
from functools import partial
from typing import Union, Optional
import eventlet
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_core.utils import CredentialManager
from eventlet import wsgi
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
from flask_socketio import SocketIO
from werkzeug.exceptions import NotFound
from bot_api.configuration.api_settings import ApiSettings
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.exception.service_error_code_enum import ServiceErrorCode
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
class Api(Flask):
def __init__(
self,
logger: ApiLogger,
services: ServiceProviderABC,
api_settings: ApiSettings,
auth_settings: AuthenticationSettings,
*args,
**kwargs,
):
if not args:
kwargs.setdefault("import_name", __name__)
Flask.__init__(self, *args, **kwargs)
self._logger = logger
self._services = services
self._api_settings = api_settings
self._auth_settings = auth_settings
self._cors = CORS(self, support_credentials=True)
# register hooks
self.before_request(self.before_request_hook)
self.after_request(self.after_request_hook)
# register error handler
exc_class, code = self._get_exc_class_and_code(Exception)
self.register_error_handler(exc_class, self.handle_exception)
# websockets
# Added async_mode see link below
# https://github.com/miguelgrinberg/Flask-SocketIO/discussions/1849
# https://stackoverflow.com/questions/39370848/flask-socket-io-sometimes-client-calls-freeze-the-server
self._socketio = SocketIO(
self, cors_allowed_origins="*", path="/api/socket.io", async_mode="eventlet"
)
self._socketio.on_event("connect", self.on_connect)
self._socketio.on_event("disconnect", self.on_disconnect)
self._socket: Optional[socket] = None
self._requests = {}
@staticmethod
def _get_methods_from_registered_route() -> Union[list[str], str]:
methods = ["Unknown"]
if (
request.path in Route.registered_routes
and len(Route.registered_routes[request.path]) >= 1
and "methods" in Route.registered_routes[request.path][1]
):
methods = Route.registered_routes[request.path][1]["methods"]
if len(methods) == 1:
return methods[0]
return methods
def _register_routes(self):
for path, f in Route.registered_routes.items():
route = f[0]
kwargs = f[1]
cls = None
qual_name_split = route.__qualname__.split(".")
if len(qual_name_split) > 0:
cls_type = vars(sys.modules[route.__module__])[qual_name_split[0]]
cls = self._services.get_service(cls_type)
partial_f = partial(route, self if cls is None else cls)
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
elif isinstance(e, NotFound):
self._logger.error(__name__, e.description)
error = ErrorDTO(ServiceErrorCode.NotFound, e.description)
return jsonify(error.to_dict()), 404
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_hook(self):
request_id = uuid.uuid4()
self._requests[request] = request_id
method = request.access_control_request_method
self._logger.info(
__name__,
f"Received {request_id} @ {self._get_methods_from_registered_route() if method is None else method} {request.url} from {request.remote_addr}",
)
headers = str(request.headers).replace("\n", "\n\t\t")
data = request.get_data()
data = "" if len(data) == 0 else str(data.decode(encoding="utf-8"))
text = textwrap.dedent(
f"Request: {request_id}:\n\tHeader:\n\t\t{headers}\n\tUser-Agent: {request.user_agent.string}\n\tBody: {data}"
)
self._logger.trace(__name__, text)
def after_request_hook(self, response: Response):
method = request.access_control_request_method
request_id = f"{self._get_methods_from_registered_route() if method is None else method} {request.url} from {request.remote_addr}"
if request in self._requests:
request_id = self._requests[request]
self._logger.info(__name__, f"Answered {request_id}")
headers = str(request.headers).replace("\n", "\n\t\t")
data = request.get_data()
data = "" if len(data) == 0 else str(data.decode(encoding="utf-8"))
text = textwrap.dedent(
f"Request: {request_id}:\n\tHeader:\n\t\t{headers}\n\tResponse: {data}"
)
self._logger.trace(__name__, text)
return response
def start(self):
self._logger.info(
__name__,
f"Starting API {self._api_settings.host}:{self._api_settings.port}",
)
self._register_routes()
self.secret_key = CredentialManager.decrypt(self._auth_settings.secret_key)
# from waitress import serve
# https://docs.pylonsproject.org/projects/waitress/en/stable/arguments.html
# serve(self, host=self._apt_settings.host, port=self._apt_settings.port, threads=10, connection_limit=1000, channel_timeout=10)
self._socket = eventlet.listen(
(self._api_settings.host, self._api_settings.port)
)
wsgi.server(self._socket, self, log_output=False)
def stop(self):
if self._socket is None:
return
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
def on_connect(self):
self._logger.info(__name__, f"Client connected")
def on_disconnect(self):
self._logger.info(__name__, f"Client disconnected")

View File

@@ -0,0 +1,57 @@
import os
from cpl_core.configuration import ConfigurationABC
from cpl_core.dependency_injection import ServiceCollectionABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClient
from cpl_discord.discord_event_types_enum import DiscordEventTypesEnum
from cpl_discord.service.discord_collection_abc import DiscordCollectionABC
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.auth_controller import AuthController
from bot_api.controller.auth_discord_controller import AuthDiscordController
from bot_api.controller.graphql_controller import GraphQLController
from bot_api.controller.gui_controller import GuiController
from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent
from bot_api.service.auth_service import AuthService
from bot_api.service.discord_service import DiscordService
from bot_core.abc.module_abc import ModuleABC
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
class ApiModule(ModuleABC):
def __init__(self, dc: DiscordCollectionABC):
ModuleABC.__init__(self, dc, FeatureFlagsEnum.api_module)
def configure_configuration(
self, config: ConfigurationABC, env: ApplicationEnvironmentABC
):
cwd = env.working_directory
env.set_working_directory(os.path.dirname(os.path.realpath(__file__)))
config.add_json_file(f"config/apisettings.json", optional=False)
config.add_json_file(
f"config/apisettings.{env.environment_name}.json", optional=True
)
config.add_json_file(f"config/apisettings.{env.host_name}.json", optional=True)
env.set_working_directory(cwd)
def configure_services(
self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC
):
services.add_singleton(EMailClientABC, EMailClient)
services.add_singleton(ApiThread)
services.add_singleton(Flask, Api)
services.add_transient(AuthServiceABC, AuthService)
services.add_transient(AuthController)
services.add_transient(AuthDiscordController)
services.add_transient(GuiController)
services.add_transient(DiscordService)
services.add_transient(GraphQLController)
# cpl-discord
services.add_transient(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent)

View File

@@ -0,0 +1,26 @@
import threading
from bot_api.api import Api
from bot_api.logging.api_logger import ApiLogger
class ApiThread(threading.Thread):
def __init__(self, logger: ApiLogger, api: Api):
threading.Thread.__init__(self, daemon=True)
self._logger = logger
self._api = api
def run(self) -> None:
try:
self._logger.trace(__name__, f"Try to start {type(self._api).__name__}")
self._api.start()
except Exception as e:
self._logger.error(__name__, "Start failed", e)
def stop(self):
try:
self._logger.trace(__name__, f"Try to stop {type(self._api).__name__}")
self._api.stop()
except Exception as e:
self._logger.error(__name__, "Stop failed", e)

View File

@@ -0,0 +1,21 @@
from cpl_core.application import ApplicationExtensionABC
from cpl_core.configuration import ConfigurationABC
from cpl_core.dependency_injection import ServiceProviderABC
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
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
Route.init_authorize()

View File

@@ -0,0 +1,44 @@
{
"ProjectSettings": {
"Name": "bot-api",
"Version": {
"Major": "1",
"Minor": "2",
"Micro": "0"
},
"Author": "",
"AuthorEmail": "",
"Description": "",
"LongDescription": "",
"URL": "",
"CopyrightDate": "",
"CopyrightName": "",
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"cpl-core==2022.12.0"
],
"DevDependencies": [
"cpl-cli==2022.12.0"
],
"PythonVersion": ">=3.10.4",
"PythonPath": {},
"Classifiers": []
},
"BuildSettings": {
"ProjectType": "library",
"SourcePath": "",
"OutputPath": "../../dist",
"Main": "bot_api.main",
"EntryPoint": "bot-api",
"IncludePackageData": false,
"Included": [],
"Excluded": [
"*/__pycache__",
"*/logs",
"*/tests"
],
"PackageData": {},
"ProjectReferences": []
}
}

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.configuration"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,22 @@
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
class ApiSettings(ConfigurationModelABC):
def __init__(self, port: int = None, host: str = None, redirect_uri: bool = None):
ConfigurationModelABC.__init__(self)
self._port = 80 if port is None else port
self._host = "" if host is None else host
self._redirect_to_https = False if redirect_uri is None else redirect_uri
@property
def port(self) -> int:
return self._port
@property
def host(self) -> str:
return self._host
@property
def redirect_to_https(self) -> bool:
return self._redirect_to_https

View File

@@ -0,0 +1,41 @@
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
class AuthenticationSettings(ConfigurationModelABC):
def __init__(
self,
secret_key: str = None,
issuer: str = None,
audience: str = None,
token_expire_time: int = None,
refresh_token_expire_time: int = None,
):
ConfigurationModelABC.__init__(self)
self._secret_key = "" if secret_key is None else secret_key
self._issuer = "" if issuer is None else issuer
self._audience = "" if audience is None else audience
self._token_expire_time = 0 if token_expire_time is None else token_expire_time
self._refresh_token_expire_time = (
0 if refresh_token_expire_time is None else refresh_token_expire_time
)
@property
def secret_key(self) -> str:
return self._secret_key
@property
def issuer(self) -> str:
return self._issuer
@property
def audience(self) -> str:
return self._audience
@property
def token_expire_time(self) -> int:
return self._token_expire_time
@property
def refresh_token_expire_time(self) -> int:
return self._refresh_token_expire_time

View File

@@ -0,0 +1,40 @@
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_query.extension import List
class DiscordAuthenticationSettings(ConfigurationModelABC):
def __init__(
self,
client_secret: str = None,
redirect_uri: str = None,
scope: list = None,
token_url: str = None,
auth_url: str = None,
):
ConfigurationModelABC.__init__(self)
self._client_secret = "" if client_secret is None else client_secret
self._redirect_url = "" if redirect_uri is None else redirect_uri
self._scope = List() if scope is None else List(str, scope)
self._token_url = "" if token_url is None else token_url
self._auth_url = "" if auth_url is None else auth_url
@property
def client_secret(self) -> str:
return self._client_secret
@property
def redirect_url(self) -> str:
return self._redirect_url
@property
def scope(self) -> List[str]:
return self._scope
@property
def token_url(self) -> str:
return self._token_url
@property
def auth_url(self) -> str:
return self._auth_url

View File

@@ -0,0 +1,12 @@
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
class FrontendSettings(ConfigurationModelABC):
def __init__(self, url: str = None):
ConfigurationModelABC.__init__(self)
self._url = "" if url is None else url
@property
def url(self) -> str:
return self._url

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.controller"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,170 @@
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClientSettings
from cpl_translation import TranslatePipe
from flask import request, jsonify, Response
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
from bot_api.json_processor import JSONProcessor
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.auth_user_dto import AuthUserDTO
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.route.route import Route
from bot_data.model.auth_role_enum import AuthRoleEnum
class AuthController:
BasePath = "/api/auth"
def __init__(
self,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
auth_service: AuthServiceABC,
):
self._config = config
self._env = env
self._logger = logger
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._auth_service = auth_service
@Route.get(f"{BasePath}/users")
@Route.authorize(role=AuthRoleEnum.admin)
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()).to_list())
@Route.post(f"{BasePath}/users/get/filtered")
@Route.authorize(role=AuthRoleEnum.admin)
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()).to_list()
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>")
@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())
@Route.post(f"{BasePath}/register")
async def register(self):
dto: AuthUserDTO = JSONProcessor.process(
AuthUserDTO, request.get_json(force=True, silent=True)
)
self._auth_service.add_auth_user(dto)
return "", 200
@Route.post(f"{BasePath}/register-by-id/<id>")
async def register_id(self, id: str):
result = await self._auth_service.confirm_email_async(id)
return jsonify(result)
@Route.post(f"{BasePath}/login")
async def login(self) -> Response:
dto: AuthUserDTO = JSONProcessor.process(
AuthUserDTO, request.get_json(force=True, silent=True)
)
result = await self._auth_service.login_async(dto)
return jsonify(result.to_dict())
@Route.get(f"{BasePath}/verify-login")
async def verify_login(self):
token = None
result = False
if "Authorization" in request.headers:
bearer = request.headers.get("Authorization")
token = bearer.split()[1]
if token is not None:
result = self._auth_service.verify_login(token)
return jsonify(result)
@Route.post(f"{BasePath}/forgot-password/<email>")
async def forgot_password(self, email: str):
await self._auth_service.forgot_password_async(email)
return "", 200
@Route.post(f"{BasePath}/confirm-forgot-password/<id>")
async def confirm_forgot_password(self, id: str):
result = await self._auth_service.confirm_forgot_password_async(id)
return jsonify(result.to_dict())
@Route.post(f"{BasePath}/reset-password")
async def reset_password(self):
dto: ResetPasswordDTO = JSONProcessor.process(
ResetPasswordDTO, request.get_json(force=True, silent=True)
)
await self._auth_service.reset_password_async(dto)
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(role=AuthRoleEnum.admin)
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_as_admin_async(dto)
return "", 200
@Route.post(f"{BasePath}/refresh")
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")
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(role=AuthRoleEnum.admin)
async def delete_user(self):
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(role=AuthRoleEnum.admin)
async def delete_user_by_mail(self, email: str):
await self._auth_service.delete_auth_user_by_email_async(email)
return "", 200

View File

@@ -0,0 +1,94 @@
import os
import uuid
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClientSettings
from cpl_core.utils import CredentialManager
from cpl_discord.service import DiscordBotServiceABC
from cpl_translation import TranslatePipe
from flask import jsonify, Response
from flask import request
from requests_oauthlib import OAuth2Session
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api
from bot_api.configuration.discord_authentication_settings import (
DiscordAuthenticationSettings,
)
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_api.route.route import Route
from bot_data.model.auth_role_enum import AuthRoleEnum
# Disable SSL requirement
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
class AuthDiscordController:
BasePath = "/api/auth/discord"
def __init__(
self,
auth_settings: DiscordAuthenticationSettings,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
bot: DiscordBotServiceABC,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
auth_service: AuthServiceABC,
):
self._auth_settings = auth_settings
self._config = config
self._env = env
self._logger = logger
self._bot = bot
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._auth_service = auth_service
def _get_user_from_discord_response(self) -> dict:
discord = OAuth2Session(
self._bot.user.id,
redirect_uri=self._auth_settings.redirect_url,
state=request.args.get("state"),
scope=self._auth_settings.scope.to_list(),
)
token = discord.fetch_token(
self._auth_settings.token_url,
client_secret=CredentialManager.decrypt(self._auth_settings.client_secret),
authorization_response=request.url,
)
discord = OAuth2Session(self._bot.user.id, token=token)
return discord.get("https://discordapp.com/api" + "/users/@me").json()
@Route.get(f"{BasePath}/get-url")
async def get_url(self):
oauth = OAuth2Session(
self._bot.user.id,
redirect_uri=self._auth_settings.redirect_url,
scope=self._auth_settings.scope.to_list(),
)
login_url, state = oauth.authorization_url(self._auth_settings.auth_url)
return jsonify({"loginUrl": login_url})
@Route.get(f"{BasePath}/login")
async def discord_login(self) -> Response:
response = self._get_user_from_discord_response()
dto = AuthUserDTO(
0,
response["username"],
response["discriminator"],
response["email"],
str(uuid.uuid4()),
None,
AuthRoleEnum.normal,
)
result = await self._auth_service.login_discord_async(dto, response["id"])
return jsonify(result.to_dict())

View File

@@ -0,0 +1,44 @@
from ariadne import graphql_sync
from ariadne.explorer import ExplorerPlayground
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from flask import request, jsonify
from bot_api.logging.api_logger import ApiLogger
from bot_api.route.route import Route
from bot_graphql.schema import Schema
class GraphQLController:
BasePath = f"/api/graphql"
def __init__(
self,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
schema: Schema,
):
self._config = config
self._env = env
self._logger = logger
self._schema = schema
@Route.get(f"{BasePath}/playground")
@Route.authorize(skip_in_dev=True)
async def playground(self):
if self._env.environment_name != "development":
return "", 403
return ExplorerPlayground().html(None), 200
@Route.post(f"{BasePath}")
@Route.authorize(by_api_key=True)
async def graphql(self):
data = request.get_json()
# Note: Passing the request to the context is optional.
# In Flask, the current request is always accessible as flask.request
success, result = graphql_sync(self._schema.schema, data, context_value=request)
return jsonify(result), 200 if success else 400

View File

@@ -0,0 +1,84 @@
import os
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMail, EMailClientABC, EMailClientSettings
from cpl_translation import TranslatePipe
from flask import jsonify
from bot_api.api import Api
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.settings_dto import SettingsDTO
from bot_api.model.version_dto import VersionDTO
from bot_api.route.route import Route
class GuiController:
BasePath = f"/api/gui"
def __init__(
self,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
auth_settings: AuthenticationSettings,
):
self._config = config
self._env = env
self._logger = logger
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._auth_settings = auth_settings
@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.get(f"{BasePath}/settings")
@Route.authorize
async def settings(self):
import bot_api
version = bot_api.version_info
return jsonify(
SettingsDTO(
"",
VersionDTO(version.major, version.minor, version.micro),
os.path.abspath(os.path.join(self._env.working_directory, "config")),
"/",
"/",
self._auth_settings.token_expire_time,
self._auth_settings.refresh_token_expire_time,
self._mail_settings.user_name,
self._mail_settings.port,
self._mail_settings.host,
self._mail_settings.user_name,
self._mail_settings.user_name,
).to_dict()
)
@Route.post(f"{BasePath}/send-test-mail/<email>")
@Route.authorize
async def send_test_mail(self, email: str):
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")
mail.add_receiver(email)
mail.subject = self._t.transform("api.api.test_mail.subject")
mail.body = self._t.transform("api.api.test_mail.message").format(
self._env.host_name, self._env.environment_name
)
self._mailer.send_mail(mail)
return "", 200

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.event"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,12 @@
from cpl_discord.events import OnReadyABC
from bot_api.api_thread import ApiThread
class BotApiOnReadyEvent(OnReadyABC):
def __init__(self, api: ApiThread):
OnReadyABC.__init__(self)
self._api = api
async def on_ready(self):
self._api.start()

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.exception"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,23 @@
from enum import Enum
from werkzeug.exceptions import Unauthorized
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
Unauthorized = 11
Forbidden = 12

View File

@@ -0,0 +1,12 @@
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}"

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.filter"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,23 @@
from bot_api.abc.select_criteria_abc import SelectCriteriaABC
class AuthUserSelectCriteria(SelectCriteriaABC):
def __init__(
self,
page_index: int,
page_size: int,
sort_direction: str,
sort_column: str,
first_name: str,
last_name: str,
email: str,
auth_role: int,
):
SelectCriteriaABC.__init__(
self, page_index, page_size, sort_direction, sort_column
)
self.first_name = first_name
self.last_name = last_name
self.email = email
self.auth_role = auth_role

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.filter.discord"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,17 @@
from bot_api.abc.select_criteria_abc import SelectCriteriaABC
class ServerSelectCriteria(SelectCriteriaABC):
def __init__(
self,
page_index: int,
page_size: int,
sort_direction: str,
sort_column: str,
name: str,
):
SelectCriteriaABC.__init__(
self, page_index, page_size, sort_direction, sort_column
)
self.name = name

View File

@@ -0,0 +1,42 @@
import enum
from inspect import signature, Parameter
from cpl_core.utils import String
class JSONProcessor:
@staticmethod
def process(_t: type, values: dict) -> object:
args = []
sig = signature(_t.__init__)
for param in sig.parameters.items():
parameter = param[1]
if parameter.name == "self" or parameter.annotation == Parameter.empty:
continue
name = String.convert_to_camel_case(parameter.name)
name = name.replace("Dto", "DTO")
name_first_lower = String.first_to_lower(name)
if name in values or name_first_lower in values:
value = ""
if name in values:
value = values[name]
else:
value = values[name_first_lower]
if isinstance(value, dict):
value = JSONProcessor.process(parameter.annotation, value)
if issubclass(parameter.annotation, enum.Enum):
value = parameter.annotation(value)
args.append(value)
elif parameter.default != Parameter.empty:
args.append(parameter.default)
else:
args.append(None)
return _t(*args)

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.logging"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,15 @@
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.time import TimeFormatSettings
from bot_core.abc.custom_file_logger_abc import CustomFileLoggerABC
class ApiLogger(CustomFileLoggerABC):
def __init__(
self,
config: ConfigurationABC,
time_format: TimeFormatSettings,
env: ApplicationEnvironmentABC,
):
CustomFileLoggerABC.__init__(self, "Api", config, time_format, env)

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.model"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,136 @@
from datetime import datetime
from typing import Optional
from cpl_query.extension import List
from bot_api.abc.dto_abc import DtoABC
from bot_api.model.user_dto import UserDTO
from bot_data.model.auth_role_enum import AuthRoleEnum
class AuthUserDTO(DtoABC):
def __init__(
self,
id: int = None,
first_name: str = None,
last_name: str = None,
email: str = None,
password: str = None,
confirmation_id: Optional[str] = None,
auth_role: AuthRoleEnum = None,
users: List[UserDTO] = None,
created_at: datetime = None,
modified_at: datetime = None,
):
DtoABC.__init__(self)
self._id = id
self._first_name = first_name
self._last_name = last_name
self._email = email
self._password = password
self._is_confirmed = confirmation_id is None
self._auth_role = auth_role
self._created_at = created_at
self._modified_at = modified_at
if users is None:
self._users = List(UserDTO)
else:
self._users = users
@property
def id(self) -> int:
return self._id
@property
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
@password.setter
def password(self, value: str):
self._password = value
@property
def is_confirmed(self) -> Optional[str]:
return self._is_confirmed
@is_confirmed.setter
def is_confirmed(self, value: Optional[str]):
self._is_confirmed = value
@property
def auth_role(self) -> AuthRoleEnum:
return self._auth_role
@auth_role.setter
def auth_role(self, value: AuthRoleEnum):
self._auth_role = value
@property
def users(self) -> List[UserDTO]:
return self._users
@property
def created_at(self) -> datetime:
return self._created_at
@property
def modified_at(self) -> datetime:
return self._modified_at
def from_dict(self, values: dict):
self._id = values["id"]
self._first_name = values["firstName"]
self._last_name = values["lastName"]
self._email = values["email"]
self._password = values["password"]
self._is_confirmed = values["isConfirmed"]
self._auth_role = AuthRoleEnum(values["authRole"])
if "users" in values:
self._users = List(UserDTO)
for u in values["users"]:
user = UserDTO()
user.from_dict(u)
self._users.add(user)
self._created_at = values["createdAt"]
self._modified_at = values["modifiedAt"]
def to_dict(self) -> dict:
return {
"id": self._id,
"firstName": self._first_name,
"lastName": self._last_name,
"email": self._email,
"password": self._password,
"isConfirmed": self._is_confirmed,
"authRole": self._auth_role.value,
"users": self._users.select(lambda u: u.to_dict()).to_list(),
"createdAt": self._created_at,
"modifiedAt": self._modified_at,
}

View File

@@ -0,0 +1,17 @@
from cpl_query.extension import List
from bot_api.abc.dto_abc import DtoABC
from bot_data.filtered_result import FilteredResult
class AuthUserFilteredResultDTO(DtoABC, FilteredResult):
def __init__(self, result: List = None, total_count: int = 0):
DtoABC.__init__(self)
FilteredResult.__init__(self, result, total_count)
def from_dict(self, values: dict):
self._result = values["users"]
self._total_count = values["totalCount"]
def to_dict(self) -> dict:
return {"users": self.result, "totalCount": self.total_count}

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.model.discord"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,56 @@
from typing import Optional
from bot_api.abc.dto_abc import DtoABC
class ServerDTO(DtoABC):
def __init__(
self,
server_id: int,
discord_id: int,
name: str,
member_count: int,
icon_url: Optional[str],
):
DtoABC.__init__(self)
self._server_id = server_id
self._discord_id = discord_id
self._name = name
self._member_count = member_count
self._icon_url = icon_url
@property
def server_id(self) -> int:
return self._server_id
@property
def discord_id(self) -> int:
return self._discord_id
@property
def name(self) -> str:
return self._name
@property
def member_count(self) -> int:
return self._member_count
@property
def icon_url(self) -> Optional[str]:
return self._icon_url
def from_dict(self, values: dict):
self._server_id = int(values["serverId"])
self._discord_id = int(values["discordId"])
self._name = values["name"]
self._icon_url = values["iconURL"]
def to_dict(self) -> dict:
return {
"serverId": self._server_id,
"discordId": self._discord_id,
"name": self._name,
"memberCount": self._member_count,
"iconURL": self._icon_url,
}

View File

@@ -0,0 +1,17 @@
from cpl_query.extension import List
from bot_api.abc.dto_abc import DtoABC
from bot_data.filtered_result import FilteredResult
class ServerFilteredResultDTO(DtoABC, FilteredResult):
def __init__(self, result: List = None, total_count: int = 0):
DtoABC.__init__(self)
FilteredResult.__init__(self, result, total_count)
def from_dict(self, values: dict):
self._result = values["servers"]
self._total_count = values["totalCount"]
def to_dict(self) -> dict:
return {"servers": self.result, "totalCount": self.total_count}

View File

@@ -0,0 +1,18 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
class EMailStringDTO(DtoABC):
def __init__(self, email: str):
DtoABC.__init__(self)
self._email = email
def from_dict(self, values: dict):
self._email = values["email"]
def to_dict(self) -> dict:
return {"email": self._email}

View File

@@ -0,0 +1,32 @@
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}

View File

@@ -0,0 +1,40 @@
from typing import Optional
from bot_api.abc.dto_abc import DtoABC
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_data.model.auth_role_enum import AuthRoleEnum
class OAuthDTO(DtoABC):
def __init__(
self,
user: AuthUserDTO,
o_auth_id: Optional[str],
):
DtoABC.__init__(self)
self._user = user
self._oauth_id = o_auth_id
@property
def user(self) -> AuthUserDTO:
return self._user
@user.setter
def user(self, value: AuthUserDTO):
self._user = value
@property
def oauth_id(self) -> Optional[str]:
return self._oauth_id
@oauth_id.setter
def oauth_id(self, value: Optional[str]):
self._oauth_id = value
def from_dict(self, values: dict):
self._user = AuthUserDTO().from_dict(values["user"])
self._oauth_id = values["oAuthId"]
def to_dict(self) -> dict:
return {"user": self._user.to_dict(), "oAuthId": self._oauth_id}

View File

@@ -0,0 +1,28 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
class ResetPasswordDTO(DtoABC):
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):
self._id = values["id"]
self._password = values["password"]
def to_dict(self) -> dict:
return {"id": self._id, "password": self._password}

View File

@@ -0,0 +1,66 @@
from bot_api.abc.dto_abc import DtoABC
from bot_api.model.version_dto import VersionDTO
class SettingsDTO(DtoABC):
def __init__(
self,
web_version: str,
api_version: VersionDTO,
config_path: str,
web_base_url: str,
api_base_url: str,
token_expire_time: int,
refresh_token_expire_time: int,
mail_user: str,
mail_port: int,
mail_host: str,
mail_transceiver: str,
mail_transceiver_address: str,
):
DtoABC.__init__(self)
self._web_version = web_version
self._api_version = api_version
self._config_path = config_path
self._web_base_url = web_base_url
self._api_base_url = api_base_url
self._token_expire_time = token_expire_time
self._refresh_token_expire_time = refresh_token_expire_time
self._mail_user = mail_user
self._mail_port = mail_port
self._mail_host = mail_host
self._mail_transceiver = mail_transceiver
self._mail_transceiver_address = mail_transceiver_address
def from_dict(self, values: dict):
self._web_version = values["webVersion"]
self._api_version.from_dict(values["apiVersion"])
self._config_path = values["configPath"]
self._web_base_url = values["webBaseURL"]
self._api_base_url = values["apiBaseURL"]
self._token_expire_time = values["tokenExpireTime"]
self._refresh_token_expire_time = values["refreshTokenExpireTime"]
self._mail_user = values["mailUser"]
self._mail_port = values["mailPort"]
self._mail_host = values["mailHost"]
self._mail_transceiver = values["mailTransceiver"]
self._mail_transceiver_address = values["mailTransceiverAddress"]
def to_dict(self) -> dict:
return {
"webVersion": self._web_version,
"apiVersion": self._api_version.str,
"configPath": self._config_path,
"webBaseURL": self._web_base_url,
"apiBaseURL": self._api_base_url,
"tokenExpireTime": self._token_expire_time,
"refreshTokenExpireTime": self._refresh_token_expire_time,
"mailUser": self._mail_user,
"mailPort": self._mail_port,
"mailHost": self._mail_host,
"mailTransceiver": self._mail_transceiver,
"mailTransceiverAddress": self._mail_transceiver_address,
}

View File

@@ -0,0 +1,34 @@
from bot_api.abc.dto_abc import DtoABC
class TokenDTO(DtoABC):
def __init__(self, token: str, refresh_token: str, first_login: bool = False):
DtoABC.__init__(self)
self._token = token
self._refresh_token = refresh_token
self._first_login = first_login
@property
def token(self) -> str:
return self._token
@property
def refresh_token(self) -> str:
return self._refresh_token
@property
def first_login(self) -> bool:
return self._first_login
def from_dict(self, values: dict):
self._token = values["token"]
self._refresh_token = values["refreshToken"]
self._first_login = values["firstLogin"]
def to_dict(self) -> dict:
return {
"token": self._token,
"refreshToken": self._refresh_token,
"firstLogin": self._first_login,
}

View File

@@ -0,0 +1,46 @@
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,
auth_user_dto: AuthUserDTO,
new_auth_user_dto: AuthUserDTO,
change_password: bool = False,
):
DtoABC.__init__(self)
self._auth_user = auth_user_dto
self._new_auth_user = new_auth_user_dto
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):
self._auth_user = AuthUserDTO().from_dict(values["authUser"])
self._new_auth_user = AuthUserDTO().from_dict(values["newAuthUser"])
self._change_password = (
False if "changePassword" not in values else bool(values["changePassword"])
)
def to_dict(self) -> dict:
return {
"authUser": self._auth_user,
"newAuthUser": self._new_auth_user,
"changePassword": self._change_password,
}

View File

@@ -0,0 +1,76 @@
from typing import Optional
from bot_api.abc.dto_abc import DtoABC
from bot_data.model.server import Server
class UserDTO(DtoABC):
def __init__(
self,
id: int = None,
dc_id: int = None,
xp: int = None,
server: Optional[Server] = None,
is_technician: Optional[bool] = None,
is_admin: Optional[bool] = None,
is_moderator: Optional[bool] = None,
):
DtoABC.__init__(self)
self._user_id = id
self._discord_id = dc_id
self._xp = xp
self._server = server
self._is_technician = is_technician
self._is_admin = is_admin
self._is_moderator = is_moderator
@property
def user_id(self) -> int:
return self._user_id
@property
def discord_id(self) -> int:
return self._discord_id
@property
def xp(self) -> int:
return self._xp
@xp.setter
def xp(self, value: int):
self._xp = value
@property
def server(self) -> Optional[Server]:
return self._server
@property
def is_technician(self) -> bool:
return self._is_technician if self._is_technician is not None else False
@property
def is_admin(self) -> bool:
return self._is_admin if self._is_admin is not None else False
@property
def is_moderator(self) -> bool:
return self._is_moderator if self._is_moderator is not None else False
def from_dict(self, values: dict):
self._user_id = values["id"]
self._discord_id = values["dcId"]
self._xp = values["xp"]
self._server = values["server"]
def to_dict(self) -> dict:
return {
"id": self._user_id,
"dcId": self._discord_id,
"xp": self._xp,
"server": self._server.id,
"isTechnician": self.is_technician,
"isAdmin": self.is_admin,
"isModerator": self.is_moderator,
}

View File

@@ -0,0 +1,42 @@
import traceback
from cpl_core.console import Console
from bot_api.abc.dto_abc import DtoABC
class VersionDTO(DtoABC):
def __init__(self, major: str = None, minor: str = None, micro: str = None):
DtoABC.__init__(self)
self._major = major
self._minor = minor
self._micro = micro
@property
def major(self) -> str:
return self._major
@property
def minor(self) -> str:
return self._minor
@property
def micro(self) -> str:
return self._micro
@property
def str(self) -> str:
return f"{self._major}.{self._minor}.{self._micro}"
def from_dict(self, values: dict):
self._major = values["major"]
self._minor = values["minor"]
self._micro = values["micro"]
def to_dict(self) -> dict:
return {
"major": self._major,
"minor": self._minor,
"micro": self._micro,
}

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.route"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,181 @@
import functools
from functools import wraps
from typing import Optional, Callable, Union
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_core.environment import ApplicationEnvironmentABC
from flask import request, jsonify
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_api.model.error_dto import ErrorDTO
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.auth_user import AuthUser
class Route:
registered_routes = {}
_auth_users: Optional[AuthUserRepositoryABC] = None
_auth: Optional[AuthServiceABC] = None
_env = "production"
@classmethod
@ServiceProviderABC.inject
def init_authorize(
cls,
env: ApplicationEnvironmentABC,
auth_users: AuthUserRepositoryABC,
auth: AuthServiceABC,
):
cls._auth_users = auth_users
cls._auth = auth
cls._env = env.environment_name
@classmethod
def get_user(cls) -> Optional[Union[str, AuthUser]]:
token = None
api_key = None
authorization = request.headers.get("Authorization").split()
match authorization[0]:
case "Bearer":
token = authorization[1]
case "API-Key":
api_key = authorization[1]
if api_key is not None:
return "system"
if token is None:
return None
jwt = cls._auth.decode_token(token)
user = cls._auth_users.get_auth_user_by_email(jwt["email"])
return user
@classmethod
def authorize(
cls,
f: Callable = None,
role: AuthRoleEnum = None,
skip_in_dev=False,
by_api_key=False,
):
if f is None:
return functools.partial(
cls.authorize, role=role, skip_in_dev=skip_in_dev, by_api_key=by_api_key
)
@wraps(f)
async def decorator(*args, **kwargs):
if skip_in_dev and cls._env == "development":
return await f(*args, **kwargs)
token = None
api_key = None
if "Authorization" in request.headers:
if " " not in request.headers.get("Authorization"):
ex = ServiceException(
ServiceErrorCode.Unauthorized, f"Token not set"
)
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
authorization = request.headers.get("Authorization").split()
match authorization[0]:
case "Bearer":
token = authorization[1]
case "API-Key":
api_key = authorization[1]
if api_key is not None:
valid = False
try:
valid = cls._auth.verify_api_key(api_key)
except ServiceException as e:
error = ErrorDTO(e.error_code, e.message)
return jsonify(error.to_dict()), 403
except Exception as e:
return jsonify(e), 500
if not valid:
ex = ServiceException(
ServiceErrorCode.Unauthorized, f"API-Key invalid"
)
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
return await f(*args, **kwargs)
if token is None:
ex = ServiceException(ServiceErrorCode.Unauthorized, f"Token not set")
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
if cls._auth_users is None or cls._auth is None:
ex = ServiceException(
ServiceErrorCode.Unauthorized, f"Authorize is not initialized"
)
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
if not cls._auth.verify_login(token):
ex = ServiceException(ServiceErrorCode.Unauthorized, f"Token expired")
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
token = cls._auth.decode_token(token)
if token is None or "email" not in token:
ex = ServiceException(ServiceErrorCode.Unauthorized, f"Token invalid")
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
user = cls._auth_users.get_auth_user_by_email(token["email"])
if user is None:
ex = ServiceException(ServiceErrorCode.Unauthorized, f"Token invalid")
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401
if role is not None and user.auth_role.value < role.value:
ex = ServiceException(
ServiceErrorCode.Unauthorized, f"Role {role} required"
)
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 403
return await f(*args, **kwargs)
return decorator
@classmethod
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
return inner
@classmethod
def get(cls, path=None, **kwargs):
return cls.route(path, methods=["GET"], **kwargs)
@classmethod
def post(cls, path=None, **kwargs):
return cls.route(path, methods=["POST"], **kwargs)
@classmethod
def head(cls, path=None, **kwargs):
return cls.route(path, methods=["HEAD"], **kwargs)
@classmethod
def put(cls, path=None, **kwargs):
return cls.route(path, methods=["PUT"], **kwargs)
@classmethod
def delete(cls, path=None, **kwargs):
return cls.route(path, methods=["DELETE"], **kwargs)

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.service"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,678 @@
import hashlib
import re
import textwrap
import uuid
from datetime import datetime, timedelta, timezone
from threading import Thread
from typing import Optional
import jwt
from cpl_core.database.context import DatabaseContextABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMail, EMailClientABC
from cpl_core.utils import CredentialManager
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
from cpl_translation import TranslatePipe
from flask import request
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
from bot_api.model.auth_user_filtered_result_dto import AuthUserFilteredResultDTO
from bot_api.model.email_string_dto import EMailStringDTO
from bot_api.model.o_auth_dto import OAuthDTO
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.api_key_repository_abc import ApiKeyRepositoryABC
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.user_repository_abc import UserRepositoryABC
from bot_data.model.api_key import ApiKey
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.auth_user import AuthUser
from bot_data.model.auth_user_users_relation import AuthUserUsersRelation
_email_regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
class AuthService(AuthServiceABC):
def __init__(
self,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
bot: DiscordBotServiceABC,
db: DatabaseContextABC,
auth_users: AuthUserRepositoryABC,
api_keys: ApiKeyRepositoryABC,
users: UserRepositoryABC,
servers: ServerRepositoryABC,
mailer: EMailClientABC,
t: TranslatePipe,
auth_settings: AuthenticationSettings,
frontend_settings: FrontendSettings,
):
AuthServiceABC.__init__(self)
self._environment = env
self._logger = logger
self._bot = bot
self._db = db
self._auth_users = auth_users
self._api_keys = api_keys
self._users = users
self._servers = servers
self._mailer = mailer
self._t = t
self._auth_settings = auth_settings
self._frontend_settings = frontend_settings
@staticmethod
def _hash_sha256(password: str, salt: str) -> str:
return hashlib.sha256(f"{password}{salt}".encode("utf-8")).hexdigest()
@staticmethod
def _is_email_valid(email: str) -> bool:
if email is None:
raise False
if re.fullmatch(_email_regex, email) is not None:
return True
return False
def _get_api_key_str(self, api_key: ApiKey) -> str:
return hashlib.sha256(
f"{api_key.identifier}:{api_key.key}+{self._auth_settings.secret_key}".encode(
"utf-8"
)
).hexdigest()
def generate_token(self, user: AuthUser) -> str:
token = jwt.encode(
payload={
"user_id": user.id,
"email": user.email,
"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=CredentialManager.decrypt(self._auth_settings.secret_key),
)
return token
def decode_token(self, token: str) -> dict:
return jwt.decode(
token,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=["HS256"],
)
def get_decoded_token_from_request(self) -> dict:
token = None
if "Authorization" in request.headers:
bearer = request.headers.get("Authorization")
token = bearer.split()[1]
if token is None:
raise ServiceException(ServiceErrorCode.Unauthorized, f"Token not set")
return jwt.decode(
token,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=["HS256"],
)
def find_decoded_token_from_request(self) -> Optional[dict]:
token = None
if "Authorization" in request.headers:
bearer = request.headers.get("Authorization")
token = bearer.split()[1]
return (
jwt.decode(
token,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=["HS256"],
)
if token is not None
else None
)
def _create_and_save_refresh_token(self, user: AuthUser) -> str:
token = str(uuid.uuid4())
user.refresh_token = token
user.refresh_token_expire_time = datetime.now() + timedelta(
days=self._auth_settings.refresh_token_expire_time
)
self._auth_users.update_auth_user(user)
self._db.save_changes()
return token
def _send_link_mail(self, email: str, subject: str, message: str):
url = self._frontend_settings.url
if not url.endswith("/"):
url = f"{url}/"
self._mailer.connect()
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")
mail.add_receiver(str(email))
mail.subject = subject
mail.body = textwrap.dedent(
f"""{message}
{self._t.transform('api.mail.automatic_mail').format(self._environment.application_name, self._environment.environment_name, self._environment.host_name)}
"""
)
thr = Thread(target=self._mailer.send_mail, args=[mail])
thr.start()
def _send_confirmation_id_to_user(self, user: AuthUser):
url = self._frontend_settings.url
if not url.endswith("/"):
url = f"{url}/"
self._send_link_mail(
user.email,
self._t.transform("api.auth.confirmation.subject").format(
user.first_name, user.last_name
),
self._t.transform("api.auth.confirmation.message").format(
url, user.confirmation_id
),
)
def _send_forgot_password_id_to_user(self, user: AuthUser):
url = self._frontend_settings.url
if not url.endswith("/"):
url = f"{url}/"
self._send_link_mail(
user.email,
self._t.transform("api.auth.forgot_password.subject").format(
user.first_name, user.last_name
),
self._t.transform("api.auth.forgot_password.message").format(
url, user.forgot_password_id
),
)
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)
async def get_filtered_auth_users_async(
self, criteria: AuthUserSelectCriteria
) -> AuthUserFilteredResultDTO:
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, with_password: bool = False
) -> AuthUserDTO:
try:
# todo: check if logged in user is admin then send mail
user = self._auth_users.get_auth_user_by_email(email)
return AUT.to_dto(user, password=user.password if with_password else None)
except Exception as e:
self._logger.error(__name__, f"AuthUser not found", e)
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)
return AUT.to_dto(user) if user is not None else None
def add_auth_user(self, user_dto: AuthUserDTO):
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")
user = AUT.to_db(user_dto)
if self._auth_users.get_all_auth_users().count() == 0:
user.auth_role = AuthRoleEnum.admin
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(user_dto.password, user.password_salt)
if not self._is_email_valid(user.email):
raise ServiceException(
ServiceErrorCode.InvalidData, "Invalid E-Mail address"
)
try:
user.confirmation_id = uuid.uuid4()
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}"
)
except Exception as e:
self._logger.error(
__name__, f"Cannot add user with E-Mail {user_dto.email}", e
)
raise ServiceException(ServiceErrorCode.UnableToAdd, "Invalid E-Mail")
async def add_auth_user_by_oauth_async(self, dto: OAuthDTO):
db_user = self._auth_users.find_auth_user_by_email(dto.user.email)
if db_user is None:
raise ServiceException(ServiceErrorCode.InvalidUser, "User not found")
if db_user.oauth_id != dto.oauth_id:
raise ServiceException(ServiceErrorCode.InvalidUser, "Wrong OAuthId")
try:
db_user.first_name = dto.user.first_name
db_user.last_name = dto.user.last_name
db_user.password_salt = uuid.uuid4()
db_user.password = self._hash_sha256(
dto.user.password, db_user.password_salt
)
db_user.oauth_id = None
db_user.confirmation_id = uuid.uuid4()
self._send_confirmation_id_to_user(db_user)
self._auth_users.update_auth_user(db_user)
self._logger.info(
__name__, f"Added auth user with E-Mail: {dto.user.email}"
)
except Exception as e:
self._logger.error(
__name__, f"Cannot add user with E-Mail {dto.user.email}", e
)
raise ServiceException(ServiceErrorCode.UnableToAdd, "Invalid E-Mail")
self._db.save_changes()
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f"User is empty")
if update_user_dto.auth_user is None:
raise ServiceException(
ServiceErrorCode.InvalidData, f"Existing user is empty"
)
if update_user_dto.new_auth_user is None:
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 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 ServiceException(ServiceErrorCode.InvalidUser, "User not found")
if user.confirmation_id is not None:
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
):
user.first_name = 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.last_name = 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 ServiceException(
ServiceErrorCode.InvalidUser, "User already exists"
)
user.email = update_user_dto.new_auth_user.email
update_user_dto.auth_user.password = self._hash_sha256(
update_user_dto.auth_user.password, user.password_salt
)
if update_user_dto.auth_user.password != user.password:
raise ServiceException(ServiceErrorCode.InvalidUser, "Wrong password")
# update password
if (
update_user_dto.new_auth_user.password is not None
and self._hash_sha256(
update_user_dto.new_auth_user.password, user.password_salt
)
!= user.password
):
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(
update_user_dto.new_auth_user.password, user.password_salt
)
self._auth_users.update_auth_user(user)
self._db.save_changes()
async def update_user_as_admin_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f"User is empty")
if update_user_dto.auth_user is None:
raise ServiceException(
ServiceErrorCode.InvalidData, f"Existing user is empty"
)
if update_user_dto.new_auth_user is None:
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 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 ServiceException(ServiceErrorCode.InvalidUser, "User not found")
if (
user.confirmation_id is not None
and update_user_dto.new_auth_user.is_confirmed
):
user.confirmation_id = None
elif (
user.confirmation_id is None
and not update_user_dto.new_auth_user.is_confirmed
):
user.confirmation_id = uuid.uuid4()
# else
# 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
):
user.first_name = 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.last_name = 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 ServiceException(
ServiceErrorCode.InvalidUser, "User already exists"
)
user.email = update_user_dto.new_auth_user.email
# update password
if (
update_user_dto.new_auth_user.password is not None
and update_user_dto.change_password
and user.password
!= self._hash_sha256(
update_user_dto.new_auth_user.password, user.password_salt
)
):
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(
update_user_dto.new_auth_user.password, user.password_salt
)
# 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._auth_users.update_auth_user(user)
self._db.save_changes()
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 ServiceException(
ServiceErrorCode.UnableToDelete, f"Cannot delete user by mail {email}"
)
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()
except Exception as e:
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"Token invalid", e)
return False
return True
def verify_api_key(self, api_key: str) -> bool:
try:
keys = self._api_keys.get_api_keys().select(self._get_api_key_str)
if not keys.contains(api_key):
raise ServiceException(ServiceErrorCode.InvalidData, "API-Key invalid")
except Exception as e:
self._logger.error(__name__, f"API-Key invalid", 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")
db_user = self._auth_users.find_auth_user_by_email(user_dto.email)
if db_user is None:
raise ServiceException(ServiceErrorCode.InvalidUser, f"User not found")
user_dto.password = self._hash_sha256(user_dto.password, db_user.password_salt)
if db_user.password != user_dto.password:
raise ServiceException(ServiceErrorCode.InvalidUser, "Wrong password")
if db_user.confirmation_id is not None:
raise ServiceException(ServiceErrorCode.Forbidden, "E-Mail not verified")
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
self._db.save_changes()
return TokenDTO(token, refresh_token)
async def login_discord_async(self, user_dto: AuthUserDTO, dc_id: int) -> TokenDTO:
if user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, "User not set")
members = self._users.get_users_by_discord_id(dc_id)
if members.count() == 0:
raise ServiceException(ServiceErrorCode.InvalidUser, f"Member not found")
added_user = False
db_user = self._auth_users.find_auth_user_by_email(user_dto.email)
if db_user is None:
self.add_auth_user(user_dto)
added_user = True
db_user = self._auth_users.get_auth_user_by_email(user_dto.email)
user_ids = db_user.users.select(lambda x: x.id)
for user in self._users.get_users_by_discord_id(dc_id):
if user.id in user_ids:
continue
self._auth_users.add_auth_user_user_rel(
AuthUserUsersRelation(db_user, user)
)
if db_user.confirmation_id is not None and not added_user:
raise ServiceException(ServiceErrorCode.Forbidden, "E-Mail not verified")
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
self._db.save_changes()
return TokenDTO(token, refresh_token, first_login=added_user)
async def refresh_async(self, token_dto: TokenDTO) -> TokenDTO:
if token_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f"Token not set")
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)
)
except Exception as e:
self._logger.error(__name__, f"Refreshing token failed", e)
return TokenDTO("", "")
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 ServiceException(ServiceErrorCode.InvalidData, "Token not set")
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")
user.refresh_token = None
self._auth_users.update_auth_user(user)
self._db.save_changes()
except Exception as e:
self._logger.error(__name__, f"Refreshing token failed", e)
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 forgot_password_async(self, email: str):
user = self._auth_users.find_auth_user_by_email(email)
if user is None:
return
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 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 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 ServiceException(
ServiceErrorCode.InvalidUser,
f"User by forgot password id {rp_dto.id} not found",
)
if user.confirmation_id is not None:
raise ServiceException(
ServiceErrorCode.InvalidUser, f"E-Mail not confirmed"
)
if user.password is None or rp_dto.password == "":
raise ServiceException(ServiceErrorCode.InvalidData, f"Password not set")
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(rp_dto.password, user.password_salt)
user.forgot_password_id = None
self._auth_users.update_auth_user(user)
self._db.save_changes()

View File

@@ -0,0 +1,104 @@
from typing import Optional
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
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_api.filter.discord.server_select_criteria import ServerSelectCriteria
from bot_api.model.discord.server_dto import ServerDTO
from bot_api.model.discord.server_filtered_result_dto import ServerFilteredResultDTO
from bot_api.transformer.server_transformer import ServerTransformer
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.user_repository_abc import UserRepositoryABC
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.server import Server
class DiscordService:
def __init__(
self,
bot: DiscordBotServiceABC,
servers: ServerRepositoryABC,
auth: AuthServiceABC,
auth_users: AuthUserRepositoryABC,
users: UserRepositoryABC,
):
self._bot = bot
self._servers = servers
self._auth = auth
self._auth_users = auth_users
self._users = users
def _to_dto(self, x: Server) -> Optional[ServerDTO]:
guild = self._bot.get_guild(x.discord_id)
if guild is None:
return ServerTransformer.to_dto(x, "", 0, None)
return ServerTransformer.to_dto(x, guild.name, guild.member_count, guild.icon)
async def get_all_servers(self) -> List[ServerDTO]:
servers = List(ServerDTO, self._servers.get_servers())
return servers.select(self._to_dto).where(lambda x: x.name != "")
async def get_all_servers_by_user(self) -> List[ServerDTO]:
token = self._auth.get_decoded_token_from_request()
if token is None or "email" not in token or "role" not in token:
raise ServiceException(ServiceErrorCode.InvalidData, "Token invalid")
role = AuthRoleEnum(token["role"])
servers = self._servers.get_servers()
if role != AuthRoleEnum.admin:
auth_user = self._auth_users.find_auth_user_by_email(token["email"])
if auth_user is not None:
user_ids = auth_user.users.select(
lambda x: x.server is not None and x.server.id
)
servers = servers.where(lambda x: x.id in user_ids)
servers = List(ServerDTO, servers)
return servers.select(self._to_dto).where(lambda x: x.name != "")
async def get_filtered_servers_async(
self, criteria: ServerSelectCriteria
) -> ServerFilteredResultDTO:
token = self._auth.get_decoded_token_from_request()
if token is None or "email" not in token or "role" not in token:
raise ServiceException(ServiceErrorCode.InvalidData, "Token invalid")
role = AuthRoleEnum(token["role"])
filtered_result = self._servers.get_filtered_servers(criteria)
# filter out servers, where the user not exists
if role != AuthRoleEnum.admin:
auth_user = self._auth_users.find_auth_user_by_email(token["email"])
if auth_user is not None:
user_ids = auth_user.users.select(
lambda x: x.server is not None and x.server.id
)
filtered_result.result = filtered_result.result.where(
lambda x: x.id in user_ids
)
servers: List = filtered_result.result.select(self._to_dto).where(
lambda x: x.name != ""
)
result = List(ServerDTO, servers)
if criteria.name is not None and criteria.name != "":
result = result.where(
lambda x: criteria.name.lower() in x.name.lower()
or x.name.lower() == criteria.name.lower()
)
return ServerFilteredResultDTO(List(ServerDTO, result), servers.count())
async def get_server_by_id_async(self, id: int) -> ServerDTO:
server = self._servers.get_server_by_id(id)
guild = self._bot.get_guild(server.discord_id)
server_dto = ServerTransformer.to_dto(
server, guild.name, guild.member_count, guild.icon
)
return server_dto

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
bot sh-edraft.de Discord bot
~~~~~~~~~~~~~~~~~~~
Discord bot for customers of sh-edraft.de
:copyright: (c) 2022 - 2023 sh-edraft.de
:license: MIT, see LICENSE for more details.
"""
__title__ = "bot_api.transformer"
__author__ = "Sven Heidemann"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2022 - 2023 sh-edraft.de"
__version__ = "1.2.0"
from collections import namedtuple
# imports:
VersionInfo = namedtuple("VersionInfo", "major minor micro")
version_info = VersionInfo(major="1", minor="2", micro="0")

View File

@@ -0,0 +1,89 @@
from datetime import datetime
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List
from bot_api.abc.transformer_abc import TransformerABC
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_api.model.user_dto import UserDTO
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.auth_user import AuthUser
from bot_data.model.user import User
from modules.permission.abc.permission_service_abc import PermissionServiceABC
class AuthUserTransformer(TransformerABC):
@staticmethod
def to_db(dto: AuthUserDTO) -> AuthUser:
return AuthUser(
dto.first_name,
dto.last_name,
dto.email,
dto.password,
None,
None,
None,
None,
None,
datetime.now(),
AuthRoleEnum.normal
if dto.auth_role is None
else AuthRoleEnum(dto.auth_role),
auth_user_id=0 if dto.id is None else dto.id,
)
@staticmethod
@ServiceProviderABC.inject
def _is_technician(
user: User, bot: DiscordBotServiceABC, permissions: PermissionServiceABC
):
guild = bot.get_guild(user.server.discord_id)
member = guild.get_member(user.discord_id)
return permissions.is_member_technician(member)
@staticmethod
@ServiceProviderABC.inject
def _is_admin(
user: User, bot: DiscordBotServiceABC, permissions: PermissionServiceABC
):
guild = bot.get_guild(user.server.discord_id)
member = guild.get_member(user.discord_id)
return permissions.is_member_admin(member)
@staticmethod
@ServiceProviderABC.inject
def _is_moderator(
user: User, bot: DiscordBotServiceABC, permissions: PermissionServiceABC
):
guild = bot.get_guild(user.server.discord_id)
member = guild.get_member(user.discord_id)
return permissions.is_member_moderator(member)
@classmethod
def to_dto(cls, db: AuthUser, password: str = None) -> AuthUserDTO:
return AuthUserDTO(
db.id,
db.first_name,
db.last_name,
db.email,
"" if password is None else password,
db.confirmation_id,
db.auth_role,
List(
UserDTO,
db.users.select(
lambda u: UserDTO(
u.id,
u.discord_id,
u.xp,
u.server,
cls._is_technician(u),
cls._is_admin(u),
cls._is_moderator(u),
)
),
),
db.created_at,
db.modified_at,
)

View File

@@ -0,0 +1,25 @@
from typing import Optional
import discord
from bot_api.abc.transformer_abc import TransformerABC
from bot_api.model.discord.server_dto import ServerDTO
from bot_data.model.server import Server
class ServerTransformer(TransformerABC):
@staticmethod
def to_db(dto: ServerDTO) -> Server:
return Server(dto.discord_id)
@staticmethod
def to_dto(
db: Server, name: str, member_count: int, icon_url: Optional[discord.Asset]
) -> ServerDTO:
return ServerDTO(
db.id,
db.discord_id,
name,
member_count,
icon_url.url if icon_url is not None else None,
)