Moved folders #405
This commit is contained in:
26
bot/src/bot_api/__init__.py
Normal file
26
bot/src/bot_api/__init__.py
Normal 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")
|
26
bot/src/bot_api/abc/__init__.py
Normal file
26
bot/src/bot_api/abc/__init__.py
Normal 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")
|
120
bot/src/bot_api/abc/auth_service_abc.py
Normal file
120
bot/src/bot_api/abc/auth_service_abc.py
Normal 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
|
15
bot/src/bot_api/abc/dto_abc.py
Normal file
15
bot/src/bot_api/abc/dto_abc.py
Normal 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
|
12
bot/src/bot_api/abc/select_criteria_abc.py
Normal file
12
bot/src/bot_api/abc/select_criteria_abc.py
Normal 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
|
17
bot/src/bot_api/abc/transformer_abc.py
Normal file
17
bot/src/bot_api/abc/transformer_abc.py
Normal 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
180
bot/src/bot_api/api.py
Normal 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")
|
57
bot/src/bot_api/api_module.py
Normal file
57
bot/src/bot_api/api_module.py
Normal 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)
|
26
bot/src/bot_api/api_thread.py
Normal file
26
bot/src/bot_api/api_thread.py
Normal 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)
|
21
bot/src/bot_api/app_api_extension.py
Normal file
21
bot/src/bot_api/app_api_extension.py
Normal 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()
|
44
bot/src/bot_api/bot-api.json
Normal file
44
bot/src/bot_api/bot-api.json
Normal 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": []
|
||||
}
|
||||
}
|
26
bot/src/bot_api/configuration/__init__.py
Normal file
26
bot/src/bot_api/configuration/__init__.py
Normal 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")
|
22
bot/src/bot_api/configuration/api_settings.py
Normal file
22
bot/src/bot_api/configuration/api_settings.py
Normal 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
|
41
bot/src/bot_api/configuration/authentication_settings.py
Normal file
41
bot/src/bot_api/configuration/authentication_settings.py
Normal 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
|
@@ -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
|
12
bot/src/bot_api/configuration/frontend_settings.py
Normal file
12
bot/src/bot_api/configuration/frontend_settings.py
Normal 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
|
26
bot/src/bot_api/controller/__init__.py
Normal file
26
bot/src/bot_api/controller/__init__.py
Normal 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")
|
170
bot/src/bot_api/controller/auth_controller.py
Normal file
170
bot/src/bot_api/controller/auth_controller.py
Normal 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
|
94
bot/src/bot_api/controller/auth_discord_controller.py
Normal file
94
bot/src/bot_api/controller/auth_discord_controller.py
Normal 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())
|
44
bot/src/bot_api/controller/graphql_controller.py
Normal file
44
bot/src/bot_api/controller/graphql_controller.py
Normal 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
|
84
bot/src/bot_api/controller/gui_controller.py
Normal file
84
bot/src/bot_api/controller/gui_controller.py
Normal 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
|
26
bot/src/bot_api/event/__init__.py
Normal file
26
bot/src/bot_api/event/__init__.py
Normal 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")
|
12
bot/src/bot_api/event/bot_api_on_ready_event.py
Normal file
12
bot/src/bot_api/event/bot_api_on_ready_event.py
Normal 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()
|
26
bot/src/bot_api/exception/__init__.py
Normal file
26
bot/src/bot_api/exception/__init__.py
Normal 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")
|
23
bot/src/bot_api/exception/service_error_code_enum.py
Normal file
23
bot/src/bot_api/exception/service_error_code_enum.py
Normal 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
|
12
bot/src/bot_api/exception/service_exception.py
Normal file
12
bot/src/bot_api/exception/service_exception.py
Normal 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}"
|
26
bot/src/bot_api/filter/__init__.py
Normal file
26
bot/src/bot_api/filter/__init__.py
Normal 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")
|
23
bot/src/bot_api/filter/auth_user_select_criteria.py
Normal file
23
bot/src/bot_api/filter/auth_user_select_criteria.py
Normal 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
|
26
bot/src/bot_api/filter/discord/__init__.py
Normal file
26
bot/src/bot_api/filter/discord/__init__.py
Normal 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")
|
17
bot/src/bot_api/filter/discord/server_select_criteria.py
Normal file
17
bot/src/bot_api/filter/discord/server_select_criteria.py
Normal 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
|
42
bot/src/bot_api/json_processor.py
Normal file
42
bot/src/bot_api/json_processor.py
Normal 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)
|
26
bot/src/bot_api/logging/__init__.py
Normal file
26
bot/src/bot_api/logging/__init__.py
Normal 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")
|
15
bot/src/bot_api/logging/api_logger.py
Normal file
15
bot/src/bot_api/logging/api_logger.py
Normal 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)
|
26
bot/src/bot_api/model/__init__.py
Normal file
26
bot/src/bot_api/model/__init__.py
Normal 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")
|
136
bot/src/bot_api/model/auth_user_dto.py
Normal file
136
bot/src/bot_api/model/auth_user_dto.py
Normal 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,
|
||||
}
|
17
bot/src/bot_api/model/auth_user_filtered_result_dto.py
Normal file
17
bot/src/bot_api/model/auth_user_filtered_result_dto.py
Normal 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}
|
26
bot/src/bot_api/model/discord/__init__.py
Normal file
26
bot/src/bot_api/model/discord/__init__.py
Normal 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")
|
56
bot/src/bot_api/model/discord/server_dto.py
Normal file
56
bot/src/bot_api/model/discord/server_dto.py
Normal 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,
|
||||
}
|
17
bot/src/bot_api/model/discord/server_filtered_result_dto.py
Normal file
17
bot/src/bot_api/model/discord/server_filtered_result_dto.py
Normal 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}
|
18
bot/src/bot_api/model/email_string_dto.py
Normal file
18
bot/src/bot_api/model/email_string_dto.py
Normal 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}
|
32
bot/src/bot_api/model/error_dto.py
Normal file
32
bot/src/bot_api/model/error_dto.py
Normal 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}
|
40
bot/src/bot_api/model/o_auth_dto.py
Normal file
40
bot/src/bot_api/model/o_auth_dto.py
Normal 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}
|
28
bot/src/bot_api/model/reset_password_dto.py
Normal file
28
bot/src/bot_api/model/reset_password_dto.py
Normal 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}
|
66
bot/src/bot_api/model/settings_dto.py
Normal file
66
bot/src/bot_api/model/settings_dto.py
Normal 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,
|
||||
}
|
34
bot/src/bot_api/model/token_dto.py
Normal file
34
bot/src/bot_api/model/token_dto.py
Normal 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,
|
||||
}
|
46
bot/src/bot_api/model/update_auth_user_dto.py
Normal file
46
bot/src/bot_api/model/update_auth_user_dto.py
Normal 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,
|
||||
}
|
76
bot/src/bot_api/model/user_dto.py
Normal file
76
bot/src/bot_api/model/user_dto.py
Normal 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,
|
||||
}
|
42
bot/src/bot_api/model/version_dto.py
Normal file
42
bot/src/bot_api/model/version_dto.py
Normal 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,
|
||||
}
|
26
bot/src/bot_api/route/__init__.py
Normal file
26
bot/src/bot_api/route/__init__.py
Normal 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")
|
181
bot/src/bot_api/route/route.py
Normal file
181
bot/src/bot_api/route/route.py
Normal 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)
|
26
bot/src/bot_api/service/__init__.py
Normal file
26
bot/src/bot_api/service/__init__.py
Normal 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")
|
678
bot/src/bot_api/service/auth_service.py
Normal file
678
bot/src/bot_api/service/auth_service.py
Normal 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()
|
104
bot/src/bot_api/service/discord_service.py
Normal file
104
bot/src/bot_api/service/discord_service.py
Normal 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
|
26
bot/src/bot_api/transformer/__init__.py
Normal file
26
bot/src/bot_api/transformer/__init__.py
Normal 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")
|
89
bot/src/bot_api/transformer/auth_user_transformer.py
Normal file
89
bot/src/bot_api/transformer/auth_user_transformer.py
Normal 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,
|
||||
)
|
25
bot/src/bot_api/transformer/server_transformer.py
Normal file
25
bot/src/bot_api/transformer/server_transformer.py
Normal 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,
|
||||
)
|
Reference in New Issue
Block a user