forked from sh-edraft.de/sh_discord_bot
		
	Reviewed-on: sh-edraft.de/kd_discord_bot#73 Closes #72
This commit is contained in:
		| @@ -10,7 +10,6 @@ | ||||
|     "DatabaseModule": true, | ||||
|     "ModeratorModule": true, | ||||
|     "PermissionModule": true, | ||||
|     "PresenceModule": true, | ||||
|     "ApiOnly": true | ||||
|     "PresenceModule": true | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from abc import ABC, abstractmethod | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_query.extension import List | ||||
|  | ||||
| @@ -23,6 +24,12 @@ class AuthServiceABC(ABC): | ||||
|     @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 | ||||
|  | ||||
|   | ||||
| @@ -11,10 +11,12 @@ 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.discord.server_controller import ServerController | ||||
| from bot_api.controller.gui_controller import GuiController | ||||
| from bot_api.controller.auth_controller import AuthController | ||||
| 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 | ||||
|  | ||||
| @@ -41,6 +43,8 @@ class ApiModule(ModuleABC): | ||||
|         services.add_transient(AuthServiceABC, AuthService) | ||||
|         services.add_transient(AuthController) | ||||
|         services.add_transient(GuiController) | ||||
|         services.add_transient(DiscordService) | ||||
|         services.add_transient(ServerController) | ||||
|  | ||||
|         # cpl-discord | ||||
|         self._dc.add_event(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent) | ||||
|   | ||||
| @@ -15,6 +15,7 @@ from bot_api.model.auth_user_dto import AuthUserDTO | ||||
| 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: | ||||
| @@ -41,13 +42,13 @@ class AuthController: | ||||
|         self._auth_service = auth_service | ||||
|  | ||||
|     @Route.get(f'{BasePath}/users') | ||||
|     @Route.authorize | ||||
|     @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())) | ||||
|  | ||||
|     @Route.post(f'{BasePath}/users/get/filtered') | ||||
|     @Route.authorize | ||||
|     @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) | ||||
| @@ -55,13 +56,13 @@ class AuthController: | ||||
|         return jsonify(result.to_dict()) | ||||
|  | ||||
|     @Route.get(f'{BasePath}/users/get/<email>') | ||||
|     @Route.authorize | ||||
|     @Route.authorize(role=AuthRoleEnum.admin) | ||||
|     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 | ||||
|     @Route.authorize(role=AuthRoleEnum.admin) | ||||
|     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()) | ||||
| @@ -109,7 +110,7 @@ class AuthController: | ||||
|         return '', 200 | ||||
|  | ||||
|     @Route.post(f'{BasePath}/update-user-as-admin') | ||||
|     @Route.authorize | ||||
|     @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) | ||||
| @@ -129,14 +130,14 @@ class AuthController: | ||||
|         return '', 200 | ||||
|  | ||||
|     @Route.post(f'{BasePath}/delete-user') | ||||
|     @Route.authorize | ||||
|     @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 | ||||
|     @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 | ||||
|   | ||||
							
								
								
									
										0
									
								
								kdb-bot/src/bot_api/controller/discord/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								kdb-bot/src/bot_api/controller/discord/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										65
									
								
								kdb-bot/src/bot_api/controller/discord/server_controller.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								kdb-bot/src/bot_api/controller/discord/server_controller.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| 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 Response, jsonify, request | ||||
|  | ||||
| from bot_api.api import Api | ||||
| from bot_api.filter.discord.server_select_criteria import ServerSelectCriteria | ||||
| from bot_api.json_processor import JSONProcessor | ||||
| from bot_api.logging.api_logger import ApiLogger | ||||
| from bot_api.route.route import Route | ||||
| from bot_api.service.discord_service import DiscordService | ||||
| from bot_data.model.auth_role_enum import AuthRoleEnum | ||||
|  | ||||
|  | ||||
| class ServerController: | ||||
|     BasePath = f'/api/discord/server' | ||||
|  | ||||
|     def __init__( | ||||
|             self, | ||||
|             config: ConfigurationABC, | ||||
|             env: ApplicationEnvironmentABC, | ||||
|             logger: ApiLogger, | ||||
|             t: TranslatePipe, | ||||
|             api: Api, | ||||
|             mail_settings: EMailClientSettings, | ||||
|             mailer: EMailClientABC, | ||||
|             discord_service: DiscordService | ||||
|     ): | ||||
|         self._config = config | ||||
|         self._env = env | ||||
|         self._logger = logger | ||||
|         self._t = t | ||||
|         self._api = api | ||||
|         self._mail_settings = mail_settings | ||||
|         self._mailer = mailer | ||||
|         self._discord_service = discord_service | ||||
|  | ||||
|     @Route.get(f'{BasePath}/get/servers') | ||||
|     @Route.authorize(role=AuthRoleEnum.admin) | ||||
|     async def get_all_servers(self) -> Response: | ||||
|         result = await self._discord_service.get_all_servers() | ||||
|         result = result.select(lambda x: x.to_dict()) | ||||
|         return jsonify(result) | ||||
|  | ||||
|     @Route.get(f'{BasePath}/get/servers-by-user') | ||||
|     @Route.authorize | ||||
|     async def get_all_servers_by_user(self) -> Response: | ||||
|         result = await self._discord_service.get_all_servers_by_user() | ||||
|         result = result.select(lambda x: x.to_dict()) | ||||
|         return jsonify(result) | ||||
|  | ||||
|     @Route.post(f'{BasePath}/get/filtered') | ||||
|     @Route.authorize | ||||
|     async def get_filtered_servers(self) -> Response: | ||||
|         dto: ServerSelectCriteria = JSONProcessor.process(ServerSelectCriteria, request.get_json(force=True, silent=True)) | ||||
|         result = await self._discord_service.get_filtered_servers_async(dto) | ||||
|         result.result = result.result.select(lambda x: x.to_dict()) | ||||
|         return jsonify(result.to_dict()) | ||||
|  | ||||
|     @Route.get(f'{BasePath}/get/<id>') | ||||
|     @Route.authorize | ||||
|     async def get_server_by_id(self, id: int) -> Response: | ||||
|         result = await self._discord_service.get_server_by_id_async(id) | ||||
|         return jsonify(result.to_dict()) | ||||
							
								
								
									
										0
									
								
								kdb-bot/src/bot_api/event/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								kdb-bot/src/bot_api/event/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -21,3 +21,4 @@ class ServiceErrorCode(Enum): | ||||
|     MailError = 10 | ||||
|  | ||||
|     Unauthorized = 11 | ||||
|     Forbidden = 12 | ||||
|   | ||||
							
								
								
									
										0
									
								
								kdb-bot/src/bot_api/filter/discord/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								kdb-bot/src/bot_api/filter/discord/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								kdb-bot/src/bot_api/filter/discord/server_select_criteria.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								kdb-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 | ||||
| @@ -15,6 +15,7 @@ class AuthUserDTO(DtoABC): | ||||
|             password: str, | ||||
|             confirmation_id: Optional[str], | ||||
|             auth_role: AuthRoleEnum, | ||||
|             user_id: Optional[int], | ||||
|     ): | ||||
|         DtoABC.__init__(self) | ||||
|  | ||||
| @@ -25,6 +26,7 @@ class AuthUserDTO(DtoABC): | ||||
|         self._password = password | ||||
|         self._is_confirmed = confirmation_id is None | ||||
|         self._auth_role = auth_role | ||||
|         self._user_id = user_id | ||||
|          | ||||
|     @property | ||||
|     def id(self) -> int: | ||||
| @@ -78,6 +80,14 @@ class AuthUserDTO(DtoABC): | ||||
|     def auth_role(self, value: AuthRoleEnum): | ||||
|         self._auth_role = value | ||||
|  | ||||
|     @property | ||||
|     def user_id(self) -> Optional[int]: | ||||
|         return self._user_id | ||||
|  | ||||
|     @user_id.setter | ||||
|     def user_id(self, value: Optional[int]): | ||||
|         self._user_id = value | ||||
|  | ||||
|     def from_dict(self, values: dict): | ||||
|         self._id = values['id'] | ||||
|         self._first_name = values['firstName'] | ||||
| @@ -86,6 +96,7 @@ class AuthUserDTO(DtoABC): | ||||
|         self._password = values['password'] | ||||
|         self._is_confirmed = values['isConfirmed'] | ||||
|         self._auth_role = values['authRole'] | ||||
|         self._user_id = values['userId'] | ||||
|  | ||||
|     def to_dict(self) -> dict: | ||||
|         return { | ||||
| @@ -96,4 +107,5 @@ class AuthUserDTO(DtoABC): | ||||
|             'password': self._password, | ||||
|             'isConfirmed': self._is_confirmed, | ||||
|             'authRole': self._auth_role.value, | ||||
|             'userId': self._user_id, | ||||
|         } | ||||
|   | ||||
							
								
								
									
										0
									
								
								kdb-bot/src/bot_api/model/discord/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								kdb-bot/src/bot_api/model/discord/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										58
									
								
								kdb-bot/src/bot_api/model/discord/server_dto.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								kdb-bot/src/bot_api/model/discord/server_dto.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| 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 = int(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, | ||||
|         } | ||||
| @@ -0,0 +1,21 @@ | ||||
| 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 | ||||
|         } | ||||
| @@ -1,5 +1,6 @@ | ||||
| import functools | ||||
| from functools import wraps | ||||
| from typing import Optional | ||||
| from typing import Optional, Callable | ||||
|  | ||||
| from flask import request, jsonify | ||||
| from flask_cors import cross_origin | ||||
| @@ -9,6 +10,7 @@ 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 | ||||
|  | ||||
|  | ||||
| class Route: | ||||
| @@ -23,7 +25,10 @@ class Route: | ||||
|         cls._auth = auth | ||||
|  | ||||
|     @classmethod | ||||
|     def authorize(cls, f): | ||||
|     def authorize(cls, f: Callable = None, role: AuthRoleEnum = None): | ||||
|         if f is None: | ||||
|             return functools.partial(cls.authorize, role=role) | ||||
|  | ||||
|         @wraps(f) | ||||
|         async def decorator(*args, **kwargs): | ||||
|             token = None | ||||
| @@ -46,6 +51,23 @@ class Route: | ||||
|                 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 | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from cpl_core.database.context import DatabaseContextABC | ||||
| from cpl_core.mailing import EMailClientABC, EMail | ||||
| from cpl_query.extension import List | ||||
| from cpl_translation import TranslatePipe | ||||
| from flask import request | ||||
|  | ||||
| from bot_api.abc.auth_service_abc import AuthServiceABC | ||||
| from bot_api.configuration.authentication_settings import AuthenticationSettings | ||||
| @@ -27,6 +28,8 @@ from bot_api.transformer.auth_user_transformer import AuthUserTransformer as AUT | ||||
| from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC | ||||
| from bot_data.model.auth_user import AuthUser | ||||
|  | ||||
| _email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' | ||||
|  | ||||
|  | ||||
| class AuthService(AuthServiceABC): | ||||
|  | ||||
| @@ -65,7 +68,7 @@ class AuthService(AuthServiceABC): | ||||
|  | ||||
|     @staticmethod | ||||
|     def _is_email_valid(email: str) -> bool: | ||||
|         if re.match(re.compile(r'^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$'), email) is not None: | ||||
|         if re.fullmatch(_email_regex, email) is not None: | ||||
|             return True | ||||
|  | ||||
|         return False | ||||
| @@ -94,6 +97,37 @@ class AuthService(AuthServiceABC): | ||||
|             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=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=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 | ||||
|   | ||||
							
								
								
									
										101
									
								
								kdb-bot/src/bot_api/service/discord_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								kdb-bot/src/bot_api/service/discord_service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_discord.service import DiscordBotServiceABC | ||||
| from cpl_query.extension import List | ||||
| from flask import jsonify | ||||
|  | ||||
| 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.model.error_dto import ErrorDTO | ||||
| from bot_api.transformer.server_transformer import ServerTransformer | ||||
| 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, | ||||
|             users: UserRepositoryABC, | ||||
|     ): | ||||
|         self._bot = bot | ||||
|         self._servers = servers | ||||
|         self._auth = auth | ||||
|         self._users = users | ||||
|  | ||||
|     def _to_dto(self, x: Server) -> Optional[ServerDTO]: | ||||
|         guild = self._bot.get_guild(x.discord_server_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']) | ||||
|         if role == AuthRoleEnum.admin: | ||||
|             servers = self._servers.get_servers() | ||||
|         else: | ||||
|             user = await self._auth.find_auth_user_by_email_async(token['email']) | ||||
|             user_from_db = self._users.find_user_by_id(0 if user.user_id is None else user.user_id) | ||||
|             servers = self._servers.get_servers().where(lambda x: user_from_db is not None and x.server_id == user_from_db.server.server_id) | ||||
|  | ||||
|         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: | ||||
|             user = await self._auth.find_auth_user_by_email_async(token['email']) | ||||
|             user_from_db = self._users.find_user_by_id(0 if user.user_id is None else user.user_id) | ||||
|             filtered_result.result = filtered_result.result.where(lambda x: user_from_db is not None and x.server_id == user_from_db.server.server_id) | ||||
|  | ||||
|         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_server_id) | ||||
|  | ||||
|         server_dto = ServerTransformer.to_dto(server, guild.name, guild.member_count, guild.icon) | ||||
|         return server_dto | ||||
| @@ -9,7 +9,7 @@ from bot_data.model.auth_user import AuthUser | ||||
| class AuthUserTransformer(TransformerABC): | ||||
|  | ||||
|     @staticmethod | ||||
|     def to_db(dto: AuthUser) -> AuthUser: | ||||
|     def to_db(dto: AuthUserDTO) -> AuthUser: | ||||
|         return AuthUser( | ||||
|             dto.first_name, | ||||
|             dto.last_name, | ||||
| @@ -19,7 +19,8 @@ class AuthUserTransformer(TransformerABC): | ||||
|             None, | ||||
|             None, | ||||
|             datetime.now(tz=timezone.utc), | ||||
|             AuthRoleEnum.normal if dto.auth_role is None else dto.auth_role, | ||||
|             AuthRoleEnum.normal if dto.auth_role is None else AuthRoleEnum(dto.auth_role), | ||||
|             dto.user_id, | ||||
|             id=0 if dto.id is None else dto.id | ||||
|         ) | ||||
|  | ||||
| @@ -32,5 +33,6 @@ class AuthUserTransformer(TransformerABC): | ||||
|             db.email, | ||||
|             db.password, | ||||
|             db.confirmation_id, | ||||
|             db.auth_role | ||||
|             db.auth_role, | ||||
|             db.user_id | ||||
|         ) | ||||
|   | ||||
							
								
								
									
										24
									
								
								kdb-bot/src/bot_api/transformer/server_transformer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								kdb-bot/src/bot_api/transformer/server_transformer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| 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.server_id, | ||||
|             db.discord_server_id, | ||||
|             name, | ||||
|             member_count, | ||||
|             icon_url.url if icon_url is not None else None, | ||||
|         ) | ||||
| @@ -3,6 +3,8 @@ from typing import Optional | ||||
|  | ||||
| from cpl_query.extension import List | ||||
|  | ||||
| from bot_api.filter.discord.server_select_criteria import ServerSelectCriteria | ||||
| from bot_data.filtered_result import FilteredResult | ||||
| from bot_data.model.server import Server | ||||
|  | ||||
|  | ||||
| @@ -13,6 +15,9 @@ class ServerRepositoryABC(ABC): | ||||
|      | ||||
|     @abstractmethod | ||||
|     def get_servers(self) -> List[Server]: pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def get_filtered_servers(self, criteria: ServerSelectCriteria) -> FilteredResult: pass | ||||
|      | ||||
|     @abstractmethod | ||||
|     def get_server_by_id(self, id: int) -> Server: pass | ||||
|   | ||||
| @@ -16,6 +16,9 @@ class UserRepositoryABC(ABC): | ||||
|      | ||||
|     @abstractmethod | ||||
|     def get_user_by_id(self, id: int) -> User: pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def find_user_by_id(self, id: int) -> Optional[User]: pass | ||||
|      | ||||
|     @abstractmethod | ||||
|     def get_users_by_discord_id(self, discord_id: int) -> List[User]: pass | ||||
|   | ||||
| @@ -28,9 +28,11 @@ class ApiMigration(MigrationABC): | ||||
|               `ForgotPasswordId` VARCHAR(255) DEFAULT NULL, | ||||
|               `RefreshTokenExpiryTime` DATETIME(6) NOT NULL, | ||||
|               `AuthRole` INT NOT NULL DEFAULT '0', | ||||
|               `UserId` BIGINT NOT NULL DEFAULT '0', | ||||
|               `CreatedOn` DATETIME(6) NOT NULL, | ||||
|               `LastModifiedOn` DATETIME(6) NOT NULL, | ||||
|               PRIMARY KEY(`Id`) | ||||
|               PRIMARY KEY(`Id`), | ||||
|               FOREIGN KEY (`UserId`) REFERENCES `Users`(`UserId`) | ||||
|             ) | ||||
|             """) | ||||
|         ) | ||||
|   | ||||
| @@ -19,6 +19,7 @@ class AuthUser(TableABC): | ||||
|             forgot_password_id: Optional[str], | ||||
|             refresh_token_expire_time: datetime, | ||||
|             auth_role: AuthRoleEnum, | ||||
|             user_id: Optional[int], | ||||
|             created_at: datetime = None, | ||||
|             modified_at: datetime = None, | ||||
|             id=0 | ||||
| @@ -34,6 +35,7 @@ class AuthUser(TableABC): | ||||
|         self._refresh_token_expire_time = refresh_token_expire_time | ||||
|  | ||||
|         self._auth_role_id = auth_role | ||||
|         self._user_id = user_id | ||||
|  | ||||
|         TableABC.__init__(self) | ||||
|         self._created_at = created_at if created_at is not None else self._created_at | ||||
| @@ -115,6 +117,14 @@ class AuthUser(TableABC): | ||||
|     def auth_role(self, value: AuthRoleEnum): | ||||
|         self._auth_role_id = value | ||||
|  | ||||
|     @property | ||||
|     def user_id(self) -> Optional[int]: | ||||
|         return self._user_id | ||||
|  | ||||
|     @user_id.setter | ||||
|     def user_id(self, value: Optional[int]): | ||||
|         self._user_id = value | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_select_all_string() -> str: | ||||
|         return str(f""" | ||||
| @@ -163,6 +173,7 @@ class AuthUser(TableABC): | ||||
|                 `ForgotPasswordId`, | ||||
|                 `RefreshTokenExpiryTime`, | ||||
|                 `AuthRole`, | ||||
|                 `UserId`, | ||||
|                 `CreatedOn`, | ||||
|                 `LastModifiedOn` | ||||
|                 ) VALUES ( | ||||
| @@ -176,6 +187,7 @@ class AuthUser(TableABC): | ||||
|                     '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}', | ||||
|                     '{self._refresh_token_expire_time}', | ||||
|                     {self._auth_role_id.value}, | ||||
|                     {"NULL" if self._user_id is None else self._user_id} | ||||
|                     '{self._created_at}',  | ||||
|                     '{self._modified_at}' | ||||
|                 ) | ||||
| @@ -194,6 +206,7 @@ class AuthUser(TableABC): | ||||
|             `ForgotPasswordId` = '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}', | ||||
|             `RefreshTokenExpiryTime` = '{self._refresh_token_expire_time}', | ||||
|             `AuthRole` = {self._auth_role_id.value}, | ||||
|             `UserId` = {"NULL" if self._user_id is None else self._user_id}, | ||||
|             `LastModifiedOn` = '{self._modified_at}' | ||||
|             WHERE `AuthUsers`.`Id` = {self._auth_user_id}; | ||||
|         """) | ||||
|   | ||||
| @@ -37,6 +37,7 @@ class AuthUserRepositoryService(AuthUserRepositoryABC): | ||||
|             self._get_value_from_result(result[7]), | ||||
|             self._get_value_from_result(result[8]), | ||||
|             AuthRoleEnum(self._get_value_from_result(result[9])), | ||||
|             self._get_value_from_result(result[10]), | ||||
|             id=self._get_value_from_result(result[0]) | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -3,8 +3,10 @@ from typing import Optional | ||||
| from cpl_core.database.context import DatabaseContextABC | ||||
| from cpl_query.extension import List | ||||
|  | ||||
| from bot_api.filter.discord.server_select_criteria import ServerSelectCriteria | ||||
| from bot_core.logging.database_logger import DatabaseLogger | ||||
| from bot_data.abc.server_repository_abc import ServerRepositoryABC | ||||
| from bot_data.filtered_result import FilteredResult | ||||
| from bot_data.model.server import Server | ||||
|  | ||||
|  | ||||
| @@ -28,6 +30,26 @@ class ServerRepositoryService(ServerRepositoryABC): | ||||
|  | ||||
|         return servers | ||||
|  | ||||
|     def get_filtered_servers(self, criteria: ServerSelectCriteria) -> FilteredResult: | ||||
|         servers = self.get_servers() | ||||
|         self._logger.trace(__name__, f'Send SQL command: {Server.get_select_all_string()}') | ||||
|         query = servers | ||||
|  | ||||
|         # sort | ||||
|         if criteria.sort_column is not None and criteria.sort_column != '' and criteria.sort_direction is not None and criteria.sort_direction: | ||||
|             crit_sort_direction = criteria.sort_direction.lower() | ||||
|             if crit_sort_direction == "desc" or crit_sort_direction == "descending": | ||||
|                 query = query.order_by_descending(lambda x: getattr(x, criteria.sort_column)) | ||||
|             else: | ||||
|                 query = query.order_by(lambda x: getattr(x, criteria.sort_column)) | ||||
|  | ||||
|         result = FilteredResult() | ||||
|         result.total_count = query.count() | ||||
|         skip = criteria.page_size * criteria.page_index | ||||
|         result.result = query.skip(skip).take(criteria.page_size) | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     def get_server_by_id(self, server_id: int) -> Server: | ||||
|         self._logger.trace(__name__, f'Send SQL command: {Server.get_select_by_id_string(server_id)}') | ||||
|         result = self._context.select(Server.get_select_by_id_string(server_id))[0] | ||||
|   | ||||
| @@ -44,6 +44,21 @@ class UserRepositoryService(UserRepositoryABC): | ||||
|             self._servers.get_server_by_id(result[3]), | ||||
|             id=result[0] | ||||
|         ) | ||||
|  | ||||
|     def find_user_by_id(self, id: int) -> Optional[User]: | ||||
|         self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_id_string(id)}') | ||||
|         result = self._context.select(User.get_select_by_id_string(id)) | ||||
|         if result is None or len(result) == 0: | ||||
|             return None | ||||
|  | ||||
|         result = result[0] | ||||
|  | ||||
|         return User( | ||||
|             result[1], | ||||
|             result[2], | ||||
|             self._servers.get_server_by_id(result[3]), | ||||
|             id=result[0] | ||||
|         ) | ||||
|      | ||||
|     def get_users_by_discord_id(self, discord_id: int) -> List[User]: | ||||
|         users = List(User) | ||||
|   | ||||
							
								
								
									
										4
									
								
								kdb-web/.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								kdb-web/.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "tabWidth": 4, | ||||
|   "useTabs": false | ||||
| } | ||||
| @@ -7,6 +7,7 @@ import { AuthGuard } from './modules/shared/guards/auth/auth.guard'; | ||||
| const routes: Routes = [ | ||||
|   { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, | ||||
|   { path: 'dashboard', loadChildren: () => import('./modules/view/dashboard/dashboard.module').then(m => m.DashboardModule), canActivate: [AuthGuard] }, | ||||
|   { path: 'server', loadChildren: () => import('./modules/view/server/server.module').then(m => m.ServerModule), canActivate: [AuthGuard] }, | ||||
|   { path: 'change-password', loadChildren: () => import('./modules/view/change-password/change-password.module').then(m => m.ChangePasswordModule), canActivate: [AuthGuard] }, | ||||
|   { path: 'user-settings', loadChildren: () => import('./modules/view/user-settings/user-settings.module').then(m => m.UserSettingsModule), canActivate: [AuthGuard] }, | ||||
|   { path: 'auth', loadChildren: () => import('./modules/auth/auth.module').then(m => m.AuthModule) }, | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| <div class="menu"> | ||||
|     <p-menu [model]="menuItems"></p-menu> | ||||
|     <p-panelMenu [model]="menuItems"></p-panelMenu> | ||||
| </div> | ||||
| @@ -1,8 +1,10 @@ | ||||
| import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { LangChangeEvent, TranslateService } from '@ngx-translate/core'; | ||||
| import { MenuItem } from 'primeng/api'; | ||||
| import { AuthRoles } from 'src/app/models/auth/auth-roles.enum'; | ||||
| import { AuthService } from 'src/app/services/auth/auth.service'; | ||||
| import { ServerService } from 'src/app/services/data/server.service'; | ||||
| import { ThemeService } from 'src/app/services/theme/theme.service'; | ||||
|  | ||||
| @Component({ | ||||
| @@ -13,13 +15,16 @@ import { ThemeService } from 'src/app/services/theme/theme.service'; | ||||
| export class SidebarComponent implements OnInit { | ||||
|  | ||||
|   isSidebarOpen: boolean = true; | ||||
|  | ||||
|   menuItems!: MenuItem[]; | ||||
|  | ||||
|   private serverId!: number; | ||||
|  | ||||
|   constructor( | ||||
|     private authService: AuthService, | ||||
|     private translateService: TranslateService, | ||||
|     private themeService: ThemeService | ||||
|     private themeService: ThemeService, | ||||
|     private route: ActivatedRoute, | ||||
|     private serverService: ServerService | ||||
|   ) { | ||||
|     this.themeService.isSidebarOpen$.subscribe(value => { | ||||
|       this.isSidebarOpen = value; | ||||
| @@ -29,6 +34,15 @@ export class SidebarComponent implements OnInit { | ||||
|     this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { | ||||
|       this.setMenu(); | ||||
|     }); | ||||
|  | ||||
|     this.serverService.server$.subscribe(server => { | ||||
|       if (!server) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this.serverId = server.serverId; | ||||
|       this.setMenu(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
| @@ -39,20 +53,40 @@ export class SidebarComponent implements OnInit { | ||||
|     this.authService.hasUserPermission(AuthRoles.Admin).then(hasPermission => { | ||||
|       this.menuItems = []; | ||||
|       this.menuItems = [ | ||||
|         { label: this.isSidebarOpen ? this.translateService.instant('sidebar.dashboard') : '', icon: 'pi pi-th-large', routerLink: 'dashboard' }, | ||||
|         { label: this.isSidebarOpen ? this.translateService.instant('sidebar.dashboard') : '', icon: 'pi pi-th-large', routerLink: 'dashboard' } | ||||
|       ]; | ||||
|  | ||||
|       if (!hasPermission) { | ||||
|         return; | ||||
|       if (this.serverId) { | ||||
|         this.addServerMenu(); | ||||
|       } | ||||
|  | ||||
|       this.menuItems.push( | ||||
|         { separator: true }, | ||||
|         { label: this.isSidebarOpen ? this.translateService.instant('sidebar.config') : '', icon: 'pi pi-cog', routerLink: '/admin/settings' }, | ||||
|         { label: this.isSidebarOpen ? this.translateService.instant('sidebar.auth_user_list') : '', icon: 'pi pi-user-edit', routerLink: '/admin/users' }, | ||||
|       ); | ||||
|       if (hasPermission) { | ||||
|         this.addAdminMenu(); | ||||
|       } | ||||
|       this.menuItems = this.menuItems.slice(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   addServerMenu() { | ||||
|     this.menuItems.push( | ||||
|       { | ||||
|         label: this.isSidebarOpen ? this.translateService.instant('sidebar.server') : '', icon: 'pi pi-server', items: [ | ||||
|           { label: this.isSidebarOpen ? this.translateService.instant('sidebar.settings') : '', icon: 'pi pi-cog', routerLink: 'server/settings' }, | ||||
|           { label: this.isSidebarOpen ? this.translateService.instant('sidebar.members') : '', icon: 'pi pi-users', routerLink: 'server/members' }, | ||||
|         ] | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   addAdminMenu() { | ||||
|     this.menuItems.push( | ||||
|       { | ||||
|         label: this.isSidebarOpen ? this.translateService.instant('sidebar.administration') : '', icon: 'pi pi-cog', items: [ | ||||
|           { label: this.isSidebarOpen ? this.translateService.instant('sidebar.config') : '', icon: 'pi pi-cog', routerLink: '/admin/settings' }, | ||||
|           { label: this.isSidebarOpen ? this.translateService.instant('sidebar.auth_user_list') : '', icon: 'pi pi-user-edit', routerLink: '/admin/users' }, | ||||
|         ] | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										7
									
								
								kdb-web/src/app/models/discord/server.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								kdb-web/src/app/models/discord/server.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export interface ServerDTO { | ||||
|     serverId: number; | ||||
|     discordId: number; | ||||
|     name: string; | ||||
|     memberCount: number; | ||||
|     iconURL: string | null; | ||||
| } | ||||
| @@ -1,8 +0,0 @@ | ||||
| import { SelectCriterion } from "../select-criterion.model"; | ||||
|  | ||||
| export interface LoginSelectCriterion extends SelectCriterion { | ||||
|     timeFrom: string; | ||||
|     timeTo: string; | ||||
|     userName: string; | ||||
|     hostName: string; | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| import { AuthUserDTO } from "../../auth/auth-user.dto"; | ||||
| import { ServerDTO } from "../../discord/server.dto"; | ||||
|  | ||||
| export interface GetFilteredServersResultDTO { | ||||
|     servers: ServerDTO[]; | ||||
|     totalCount: number; | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| import { SelectCriterion } from "../select-criterion.model"; | ||||
|  | ||||
| export interface ServerSelectCriterion extends SelectCriterion { | ||||
|     name: string | null; | ||||
| } | ||||
| @@ -85,7 +85,7 @@ export class AuthUserComponent implements OnInit { | ||||
|     }; | ||||
|  | ||||
|     this.setFilterForm(); | ||||
|     // this.loadNextPage(); | ||||
|     this.loadNextPage(); | ||||
|   } | ||||
|  | ||||
|   setFilterForm() { | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import { ToastModule } from 'primeng/toast'; | ||||
| import { AuthRolePipe } from './pipes/auth-role.pipe'; | ||||
| import { IpAddressPipe } from './pipes/ip-address.pipe'; | ||||
| import { BoolPipe } from './pipes/bool.pipe'; | ||||
| import { PanelMenuModule } from 'primeng/panelmenu'; | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -44,7 +45,8 @@ import { BoolPipe } from './pipes/bool.pipe'; | ||||
|     CheckboxModule, | ||||
|     DropdownModule, | ||||
|     TranslateModule, | ||||
|     DynamicDialogModule | ||||
|     DynamicDialogModule, | ||||
|     PanelMenuModule, | ||||
|   ], | ||||
|   exports: [ | ||||
|     ButtonModule, | ||||
| @@ -63,6 +65,7 @@ import { BoolPipe } from './pipes/bool.pipe'; | ||||
|     DropdownModule, | ||||
|     TranslateModule, | ||||
|     DynamicDialogModule, | ||||
|     PanelMenuModule, | ||||
|     AuthRolePipe, | ||||
|     IpAddressPipe, | ||||
|     BoolPipe, | ||||
|   | ||||
| @@ -1,3 +1,50 @@ | ||||
| <p>dashboard works!</p> | ||||
| <h1> | ||||
|     {{'view.dashboard.header' | translate}} | ||||
| </h1> | ||||
| <div class="content-wrapper"> | ||||
|     <div class="content-header"> | ||||
|         <h2> | ||||
|             <i class="pi pi-server"></i> | ||||
|             {{'view.dashboard.server.header' | translate}} | ||||
|         </h2> | ||||
|     </div> | ||||
|  | ||||
| <div class="content"></div> | ||||
|     <div class="content"> | ||||
|         <div class="server-list-wrapper"> | ||||
|             <div class="server-filter"> | ||||
|                 <form [formGroup]="filterForm"> | ||||
|                     <div class="input-field"> | ||||
|                         <input type="text" pInputText formControlName="name" | ||||
|                             placeholder="{{'view.dashboard.filter.name' | translate}}" autocomplete="given-name"> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </div> | ||||
|  | ||||
|             <div class="server-count"> | ||||
|                 {{servers.length}} {{'view.dashboard.of' | translate}} {{totalRecords}} {{'view.dashboard.servers' | translate}}: | ||||
|                 <hr> | ||||
|             </div> | ||||
|  | ||||
|             <div class="server-list"> | ||||
|                 <div class="server" *ngFor="let server of servers" (click)="selectServer(server)"> | ||||
|                     <div class="logo"> | ||||
|                         <img *ngIf="server.iconURL" [src]="server.iconURL"> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="info"> | ||||
|                         <h3 class="name"> | ||||
|                             {{server.name}} | ||||
|                         </h3> | ||||
|  | ||||
|                         <div class="data"> | ||||
|                             <i class="pi pi-users"></i> | ||||
|                             {{server.memberCount}} | ||||
|                             {{'view.dashboard.server.member_count' | translate}} | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -1,4 +1,16 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { LazyLoadEvent } from 'primeng/api'; | ||||
| import { catchError, debounceTime, throwError } from 'rxjs'; | ||||
| import { ServerDTO } from 'src/app/models/discord/server.dto'; | ||||
| import { ServerSelectCriterion } from 'src/app/models/selection/server/server-select-criterion.dto'; | ||||
| import { ConfirmationDialogService } from 'src/app/services/confirmation-dialog/confirmation-dialog.service'; | ||||
| import { DataService } from 'src/app/services/data/data.service'; | ||||
| import { ServerService } from 'src/app/services/data/server.service'; | ||||
| import { SpinnerService } from 'src/app/services/spinner/spinner.service'; | ||||
| import { ToastService } from 'src/app/services/toast/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-dashboard', | ||||
| @@ -7,8 +19,97 @@ import { Component, OnInit } from '@angular/core'; | ||||
| }) | ||||
| export class DashboardComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|   servers: ServerDTO[] = []; | ||||
|  | ||||
|   ngOnInit(): void {} | ||||
|   searchCriterions: ServerSelectCriterion = { | ||||
|     name: null, | ||||
|     pageIndex: 0, | ||||
|     pageSize: 10, | ||||
|     sortColumn: null, | ||||
|     sortDirection: null | ||||
|   }; | ||||
|   totalRecords!: number; | ||||
|  | ||||
|  | ||||
|   filterForm!: FormGroup<{ | ||||
|     name: FormControl<string | null>, | ||||
|   }>; | ||||
|  | ||||
|   constructor( | ||||
|     private data: DataService, | ||||
|     private spinnerService: SpinnerService, | ||||
|     private toastService: ToastService, | ||||
|     private confirmDialog: ConfirmationDialogService, | ||||
|     private fb: FormBuilder, | ||||
|     private translate: TranslateService, | ||||
|     private router: Router, | ||||
|     private serverService: ServerService | ||||
|   ) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.spinnerService.showSpinner(); | ||||
|     this.setFilterForm(); | ||||
|     this.loadNextPage(); | ||||
|   } | ||||
|  | ||||
|   setFilterForm() { | ||||
|     this.filterForm = this.fb.group({ | ||||
|       name: [''], | ||||
|     }); | ||||
|  | ||||
|     this.filterForm.valueChanges.pipe( | ||||
|       debounceTime(600) | ||||
|     ).subscribe(changes => { | ||||
|       if (changes.name) { | ||||
|         this.searchCriterions.name = changes.name; | ||||
|       } else { | ||||
|         this.searchCriterions.name = null; | ||||
|       } | ||||
|  | ||||
|       if (this.searchCriterions.pageSize) | ||||
|         this.searchCriterions.pageSize = 10; | ||||
|  | ||||
|       if (this.searchCriterions.pageSize) | ||||
|         this.searchCriterions.pageIndex = 0; | ||||
|  | ||||
|       this.loadNextPage(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   loadNextPage() { | ||||
|     this.spinnerService.showSpinner(); | ||||
|     this.data.getFilteredServers(this.searchCriterions).pipe(catchError(err => { | ||||
|       this.spinnerService.hideSpinner(); | ||||
|       return throwError(() => err); | ||||
|     })).subscribe(list => { | ||||
|       this.totalRecords = list.totalCount; | ||||
|       this.servers = list.servers; | ||||
|       this.spinnerService.hideSpinner(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   nextPage(event: LazyLoadEvent) { | ||||
|     this.searchCriterions.pageSize = event.rows ?? 0; | ||||
|     if (event.first != null && event.rows != null) | ||||
|       this.searchCriterions.pageIndex = event.first / event.rows; | ||||
|     this.searchCriterions.sortColumn = event.sortField ?? null; | ||||
|     this.searchCriterions.sortDirection = event.sortOrder === 1 ? 'asc' : event.sortOrder === -1 ? 'desc' : 'asc'; | ||||
|  | ||||
|     if (event.filters) { | ||||
|       // + "" => convert to string | ||||
|       this.searchCriterions.name = event.filters['name'] ? event.filters['name'] + "" : null; | ||||
|     } | ||||
|  | ||||
|     this.loadNextPage(); | ||||
|   } | ||||
|  | ||||
|   resetFilters() { | ||||
|     this.filterForm.reset(); | ||||
|   } | ||||
|  | ||||
|   selectServer(server: ServerDTO) { | ||||
|     this.serverService.server$.next(server); | ||||
|     this.router.navigate(['/server']); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component'; | ||||
|  | ||||
| const routes: Routes = [ | ||||
|    {path: '', component: DashboardComponent} | ||||
|   { path: '', component: DashboardComponent } | ||||
| ]; | ||||
|  | ||||
| @NgModule({ | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { NgModule } from '@angular/core'; | ||||
|  | ||||
| import { DashboardRoutingModule } from './dashboard-routing.module'; | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component'; | ||||
| import { SharedModule } from '../../shared/shared.module'; | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component'; | ||||
| import { DashboardRoutingModule } from './dashboard-routing.module'; | ||||
|  | ||||
|  | ||||
| @NgModule({ | ||||
|   | ||||
| @@ -0,0 +1,36 @@ | ||||
| <h1> | ||||
|     {{'view.dashboard.header' | translate}} | ||||
| </h1> | ||||
| <div class="content-wrapper"> | ||||
|     <div class="content-header"> | ||||
|         <h2> | ||||
|             <i class="pi pi-server"></i> | ||||
|             {{'view.dashboard.server.header' | translate}} | ||||
|         </h2> | ||||
|     </div> | ||||
|  | ||||
|     <div class="content"> | ||||
|         <div class="server-list-wrapper"> | ||||
|             <div class="server-list"> | ||||
|                 <div class="server"> | ||||
|                     <div class="logo"> | ||||
|                         <img *ngIf="server.iconURL" [src]="server.iconURL"> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="info"> | ||||
|                         <h3 class="name"> | ||||
|                             {{server.name}} | ||||
|                         </h3> | ||||
|  | ||||
|                         <div class="data"> | ||||
|                             <i class="pi pi-users"></i> | ||||
|                             {{server.memberCount}} | ||||
|                             {{'view.dashboard.server.member_count' | translate}} | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -0,0 +1,23 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ServerDashboardComponent } from './server-dashboard.component'; | ||||
|  | ||||
| describe('ServerDashboardComponent', () => { | ||||
|   let component: ServerDashboardComponent; | ||||
|   let fixture: ComponentFixture<ServerDashboardComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ ServerDashboardComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|  | ||||
|     fixture = TestBed.createComponent(ServerDashboardComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,38 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { ServerDTO } from 'src/app/models/discord/server.dto'; | ||||
| import { DataService } from 'src/app/services/data/data.service'; | ||||
| import { ServerService } from 'src/app/services/data/server.service'; | ||||
| import { SpinnerService } from 'src/app/services/spinner/spinner.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-server-dashboard', | ||||
|   templateUrl: './server-dashboard.component.html', | ||||
|   styleUrls: ['./server-dashboard.component.scss'] | ||||
| }) | ||||
| export class ServerDashboardComponent implements OnInit { | ||||
|  | ||||
|   id!: number; | ||||
|   server!: ServerDTO; | ||||
|  | ||||
|   constructor( | ||||
|     private route: ActivatedRoute, | ||||
|     private router: Router, | ||||
|     private data: DataService, | ||||
|     private spinner: SpinnerService, | ||||
|     private serverService: ServerService | ||||
|   ) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.spinner.showSpinner(); | ||||
|     if (!this.serverService.server$.value) { | ||||
|       this.spinner.hideSpinner(); | ||||
|       this.router.navigate(['/dashboard']); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this.server = this.serverService.server$.value; | ||||
|     this.spinner.hideSpinner(); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										13
									
								
								kdb-web/src/app/modules/view/server/server-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								kdb-web/src/app/modules/view/server/server-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { ServerDashboardComponent } from './server-dashboard/server-dashboard.component'; | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   { path: '', component: ServerDashboardComponent } | ||||
| ]; | ||||
|  | ||||
| @NgModule({ | ||||
|   imports: [RouterModule.forChild(routes)], | ||||
|   exports: [RouterModule] | ||||
| }) | ||||
| export class ServerRoutingModule { } | ||||
							
								
								
									
										19
									
								
								kdb-web/src/app/modules/view/server/server.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								kdb-web/src/app/modules/view/server/server.module.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { ServerDashboardComponent } from './server-dashboard/server-dashboard.component'; | ||||
| import { ServerRoutingModule } from './server-routing.module'; | ||||
| import { SharedModule } from '../../shared/shared.module'; | ||||
|  | ||||
|  | ||||
|  | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
|     ServerDashboardComponent | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
|     ServerRoutingModule, | ||||
|     SharedModule | ||||
|   ] | ||||
| }) | ||||
| export class ServerModule { } | ||||
| @@ -1,5 +1,9 @@ | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { HttpClient, HttpHeaders } from '@angular/common/http'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { ServerDTO } from 'src/app/models/discord/server.dto'; | ||||
| import { GetFilteredServersResultDTO } from 'src/app/models/selection/server/get-filtered-servers-result.dto'; | ||||
| import { ServerSelectCriterion } from 'src/app/models/selection/server/server-select-criterion.dto'; | ||||
| import { SettingsService } from '../settings/settings.service'; | ||||
|  | ||||
| @Injectable({ | ||||
| @@ -11,4 +15,39 @@ export class DataService { | ||||
|     private appsettings: SettingsService, | ||||
|     private http: HttpClient, | ||||
|   ) { } | ||||
|  | ||||
|  | ||||
|  | ||||
|   /* data requests */ | ||||
|   getAllServers(): Observable<Array<ServerDTO>> { | ||||
|     return this.http.get<Array<ServerDTO>>(`${this.appsettings.getApiURL()}/api/discord/server/servers`, { | ||||
|       headers: new HttpHeaders({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   getAllServersByUser(): Observable<Array<ServerDTO>> { | ||||
|     return this.http.get<Array<ServerDTO>>(`${this.appsettings.getApiURL()}/api/discord/server/servers-by-user`, { | ||||
|       headers: new HttpHeaders({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getFilteredServers(selectCriterions: ServerSelectCriterion): Observable<GetFilteredServersResultDTO> { | ||||
|     return this.http.post<GetFilteredServersResultDTO>(`${this.appsettings.getApiURL()}/api/discord/server/get/filtered`, selectCriterions, { | ||||
|       headers: new HttpHeaders({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   getServerByID(id: number): Observable<ServerDTO> { | ||||
|     return this.http.get<ServerDTO>(`${this.appsettings.getApiURL()}/api/discord/server/get/${id}`, { | ||||
|       headers: new HttpHeaders({ | ||||
|         'Content-Type': 'application/json' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								kdb-web/src/app/services/data/server.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								kdb-web/src/app/services/data/server.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import { TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ServerService } from './server.service'; | ||||
|  | ||||
| describe('ServerService', () => { | ||||
|   let service: ServerService; | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({}); | ||||
|     service = TestBed.inject(ServerService); | ||||
|   }); | ||||
|  | ||||
|   it('should be created', () => { | ||||
|     expect(service).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										23
									
								
								kdb-web/src/app/services/data/server.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								kdb-web/src/app/services/data/server.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { BehaviorSubject, Subject } from 'rxjs'; | ||||
| import { ServerDTO } from 'src/app/models/discord/server.dto'; | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class ServerService { | ||||
|  | ||||
|   private server!: ServerDTO; | ||||
|   server$ = new BehaviorSubject<ServerDTO | null>(null); | ||||
|  | ||||
|   constructor() { | ||||
|     this.server$.subscribe(server => { | ||||
|       if (!server) { | ||||
|         return; | ||||
|       } | ||||
|       this.server = server; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -27,7 +27,7 @@ export class ThemeService { | ||||
|     }); | ||||
|     this.isSidebarOpen$.subscribe(isSidebarOpen => { | ||||
|       this.isSidebarOpen = isSidebarOpen; | ||||
|       this.sidebarWidth$.next(isSidebarOpen ? '150px' : '50px'); | ||||
|       this.sidebarWidth$.next(isSidebarOpen ? '175px' : '75px'); | ||||
|     }); | ||||
|     this.sidebarWidth$.subscribe(sidebarWidth => { | ||||
|       this.sidebarWidth = sidebarWidth; | ||||
|   | ||||
| @@ -7,10 +7,11 @@ | ||||
|     }, | ||||
|     "sidebar": { | ||||
|         "dashboard": "Dashboard", | ||||
|         "domain_list": "Domänen", | ||||
|         "host_list": "Rechner", | ||||
|         "user_list": "Benutzer", | ||||
|         "login_list": "Logins", | ||||
|         "server": "Server", | ||||
|         "server_empty": "Kein Server ausgewählt", | ||||
|         "settings": "Einstellungen", | ||||
|         "members": "Mitglieder", | ||||
|         "administration": "Administration", | ||||
|         "config": "Konfiguration", | ||||
|         "auth_user_list": "Benutzer" | ||||
|     }, | ||||
| @@ -106,7 +107,21 @@ | ||||
|         } | ||||
|     }, | ||||
|     "view": { | ||||
|         "dashboard": {}, | ||||
|         "dashboard": { | ||||
|             "header": "Dashboard", | ||||
|             "of": "von", | ||||
|             "servers": "Server", | ||||
|             "server": { | ||||
|                 "header": "Server", | ||||
|                 "member_count": "Mitglid(er)" | ||||
|             }, | ||||
|             "filter": { | ||||
|                 "name": "Name" | ||||
|             } | ||||
|         }, | ||||
|         "server": { | ||||
|             "header": "Server" | ||||
|         }, | ||||
|         "user-list": {}, | ||||
|         "change-password": { | ||||
|             "header": "Passwort ändern", | ||||
| @@ -200,24 +215,8 @@ | ||||
|             "Freitag", | ||||
|             "Samstag" | ||||
|         ], | ||||
|         "dayNamesShort": [ | ||||
|             "Son", | ||||
|             "Mon", | ||||
|             "Die", | ||||
|             "Mit", | ||||
|             "Don", | ||||
|             "Fre", | ||||
|             "Sam" | ||||
|         ], | ||||
|         "dayNamesMin": [ | ||||
|             "So", | ||||
|             "Mo", | ||||
|             "Di", | ||||
|             "Mi", | ||||
|             "Do", | ||||
|             "Fr", | ||||
|             "Sa" | ||||
|         ], | ||||
|         "dayNamesShort": ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam"], | ||||
|         "dayNamesMin": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], | ||||
|         "monthNames": [ | ||||
|             "Januar", | ||||
|             "Februar", | ||||
| @@ -255,4 +254,4 @@ | ||||
|         "emptyMessage": "Keine Ergebnisse gefunden", | ||||
|         "emptyFilterMessage": "Keine Ergebnisse gefunden" | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -10,6 +10,8 @@ body { | ||||
|     height: 100%; | ||||
|     padding: 0; | ||||
|     margin: 0; | ||||
|  | ||||
|     font-size: 1rem; | ||||
| } | ||||
|  | ||||
| main { | ||||
| @@ -18,15 +20,19 @@ main { | ||||
|     min-height: 100vh; | ||||
| } | ||||
|  | ||||
| h1, | ||||
| h2 { | ||||
| h1 { | ||||
|     margin: 0; | ||||
|     font-size: 30px; | ||||
|     font-size: 1.75rem; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|     margin: 0; | ||||
|     font-size: 25px; | ||||
|     font-size: 1.5rem; | ||||
| } | ||||
|  | ||||
| h3 { | ||||
|     margin: 0; | ||||
|     font-size: 1.25rem; | ||||
| } | ||||
|  | ||||
| header { | ||||
| @@ -207,6 +213,59 @@ header { | ||||
|                     .table-header-small-dropdown { | ||||
|                         width: 150px; | ||||
|                     } | ||||
|  | ||||
|                     .server-list-wrapper { | ||||
|                         display: flex; | ||||
|                         flex-direction: column; | ||||
|                         gap: 10px; | ||||
|  | ||||
|                         .server-filter { | ||||
|                         } | ||||
|  | ||||
|                         .server-count { | ||||
|                         } | ||||
|  | ||||
|                         .server-list { | ||||
|                             display: flex; | ||||
|                             flex-direction: column; | ||||
|  | ||||
|                             gap: 15px; | ||||
|  | ||||
|                             .server { | ||||
|                                 display: flex; | ||||
|                                 gap: 15px; | ||||
|  | ||||
|                                 padding: 20px; | ||||
|  | ||||
|                                 .logo { | ||||
|                                     overflow: hidden; | ||||
|  | ||||
|                                     img { | ||||
|                                         width: 4rem; | ||||
|                                         height: 4rem; | ||||
|                                         object-fit: contain; | ||||
|                                     } | ||||
|                                 } | ||||
|  | ||||
|                                 .info { | ||||
|                                     display: flex; | ||||
|                                     flex-direction: column; | ||||
|  | ||||
|                                     gap: 10px; | ||||
|  | ||||
|                                     .name { | ||||
|                                         margin: 0px; | ||||
|  | ||||
|                                         justify-content: center; | ||||
|                                         align-items: center; | ||||
|                                     } | ||||
|  | ||||
|                                     .data { | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -6,14 +6,17 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .p-menu { | ||||
| .p-menu, | ||||
| .p-panelmenu { | ||||
|     background: none !important; | ||||
|     border: none !important; | ||||
|     width: auto !important; | ||||
|     border-radius: 0px !important; | ||||
|     padding: 0 !important; | ||||
|  | ||||
|     .p-menuitem-link { | ||||
|     .p-menuitem-link, | ||||
|     .p-panelmenu-header > a, | ||||
|     .p-panelmenu-content .p-menuitem .p-menuitem-link { | ||||
|         $distance: 10px; | ||||
|         padding: $distance 0px $distance $distance !important; | ||||
|         margin: 4px 0px 4px 6px !important; | ||||
| @@ -24,6 +27,31 @@ | ||||
|     top: $headerHeight !important; | ||||
| } | ||||
|  | ||||
| .p-panelmenu { | ||||
|     .p-panelmenu-icon { | ||||
|         order: 1; // to be the first item on right side. | ||||
|     } | ||||
|     .p-menuitem-text { | ||||
|         flex-grow: 1; // to fill the whole space and push the icon to the end | ||||
|     } | ||||
|  | ||||
|     .p-panelmenu-header > a { | ||||
|         border: none !important; | ||||
|         border-radius: none !important; | ||||
|         font-weight: none !important; | ||||
|         transition: none !important; | ||||
|     } | ||||
|  | ||||
|     .p-panelmenu-content { | ||||
|         border: none !important; | ||||
|         background: none !important; | ||||
|     } | ||||
|  | ||||
|     .p-menuitem-text { | ||||
|         line-height: normal !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| ui-menu .ui-menu-parent .ui-menu-child { | ||||
|     width: 400px; /* exagerated !! */ | ||||
| } | ||||
|   | ||||
| @@ -15,16 +15,9 @@ | ||||
|     $primaryErrorColor: #b00020; | ||||
|     $secondaryErrorColor: #e94948; | ||||
|  | ||||
|     $default-border: 1px solid $secondaryBackgroundColor3; | ||||
|     $default-border: 2px solid $secondaryBackgroundColor3; | ||||
|  | ||||
|     background-color: $primaryBackgroundColor !important; | ||||
|  | ||||
|     html, | ||||
|     body { | ||||
|         margin: 0; | ||||
|  | ||||
|         font-size: 16px; | ||||
|     } | ||||
|     background-color: $primaryBackgroundColor; | ||||
|  | ||||
|     h1, | ||||
|     h2 { | ||||
| @@ -122,6 +115,35 @@ | ||||
|                         .content-divider { | ||||
|                             border-bottom: $default-border; | ||||
|                         } | ||||
|  | ||||
|                         .server-list-wrapper { | ||||
|                             .server-filter { | ||||
|                             } | ||||
|  | ||||
|                             .server-count { | ||||
|                             } | ||||
|  | ||||
|                             .server-list { | ||||
|                                 .server { | ||||
|                                     border: $default-border; | ||||
|                                     border-radius: 15px; | ||||
|  | ||||
|                                     .logo { | ||||
|                                         img { | ||||
|                                             border-radius: 100%; | ||||
|                                         } | ||||
|                                     } | ||||
|  | ||||
|                                     .name { | ||||
|                                         color: $primaryHeaderColor; | ||||
|                                     } | ||||
|  | ||||
|                                     &:hover { | ||||
|                                         border-color: $primaryHeaderColor !important; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -228,28 +250,43 @@ | ||||
|         stroke: $primaryHeaderColor !important; | ||||
|     } | ||||
|  | ||||
|     .p-menu { | ||||
|     .p-menu, | ||||
|     .p-panelmenu { | ||||
|         color: $primaryTextColor !important; | ||||
|  | ||||
|         .p-menuitem-link .p-menuitem-text, | ||||
|         .p-menuitem-link .p-menuitem-icon { | ||||
|         .p-menuitem-link .p-menuitem-icon, | ||||
|         .p-panelmenu-header > a { | ||||
|             color: $primaryTextColor !important; | ||||
|             background: transparent !important; | ||||
|             font-size: 1rem !important; | ||||
|             font-weight: normal !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:focus { | ||||
|         .p-menuitem-link:focus, | ||||
|         .p-panelmenu-header > a:focus, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:hover { | ||||
|         .p-menuitem-link:hover, | ||||
|         .p-panelmenu-header > a:hover, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:hover { | ||||
|             background-color: $secondaryBackgroundColor !important; | ||||
|             $border-radius: 20px; | ||||
|             border-radius: $border-radius 0px 0px $border-radius; | ||||
|  | ||||
|             .p-menuitem-text, | ||||
|             .p-menuitem-icon { | ||||
|             .p-menuitem-icon, | ||||
|             .p-menuitem-text, | ||||
|             .p-panelmenu-icon { | ||||
|                 color: $primaryHeaderColor !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .p-panelmenu-content { | ||||
|             margin: 5px 0px 5px 10px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .p-menu-overlay { | ||||
| @@ -388,6 +425,9 @@ | ||||
|  | ||||
|     input, | ||||
|     .p-password { | ||||
|         border-radius: 10px; | ||||
|         border: $default-border; | ||||
|  | ||||
|         &:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
| @@ -432,7 +472,7 @@ | ||||
|         color: $primaryTextColor !important; | ||||
|         border: 0 !important; | ||||
|         padding: 0px !important; | ||||
|          | ||||
|  | ||||
|         &:hover { | ||||
|             background-color: transparent !important; | ||||
|             color: $primaryHeaderColor !important; | ||||
|   | ||||
| @@ -15,14 +15,9 @@ | ||||
|     $primaryErrorColor: #b00020; | ||||
|     $secondaryErrorColor: #e94948; | ||||
|  | ||||
|     $default-border: 1px solid $secondaryBackgroundColor; | ||||
|     $default-border: 2px solid $secondaryBackgroundColor; | ||||
|  | ||||
|     html, | ||||
|     body { | ||||
|         margin: 0; | ||||
|  | ||||
|         font-size: 16px; | ||||
|     } | ||||
|     background-color: $primaryBackgroundColor; | ||||
|  | ||||
|     h1, | ||||
|     h2 { | ||||
| @@ -120,6 +115,35 @@ | ||||
|                         .content-divider { | ||||
|                             border-bottom: $default-border; | ||||
|                         } | ||||
|  | ||||
|                         .server-list-wrapper { | ||||
|                             .server-filter { | ||||
|                             } | ||||
|  | ||||
|                             .server-count { | ||||
|                             } | ||||
|  | ||||
|                             .server-list { | ||||
|                                 .server { | ||||
|                                     border: $default-border; | ||||
|                                     border-radius: 15px; | ||||
|  | ||||
|                                     .logo { | ||||
|                                         img { | ||||
|                                             border-radius: 100%; | ||||
|                                         } | ||||
|                                     } | ||||
|  | ||||
|                                     .name { | ||||
|                                         color: $primaryHeaderColor; | ||||
|                                     } | ||||
|  | ||||
|                                     &:hover { | ||||
|                                         border-color: $primaryHeaderColor !important; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -226,28 +250,43 @@ | ||||
|         stroke: $primaryHeaderColor !important; | ||||
|     } | ||||
|  | ||||
|     .p-menu { | ||||
|     .p-menu, | ||||
|     .p-panelmenu { | ||||
|         color: $primaryTextColor !important; | ||||
|  | ||||
|         .p-menuitem-link .p-menuitem-text, | ||||
|         .p-menuitem-link .p-menuitem-icon { | ||||
|         .p-menuitem-link .p-menuitem-icon, | ||||
|         .p-panelmenu-header > a { | ||||
|             color: $primaryTextColor !important; | ||||
|             background: transparent !important; | ||||
|             font-size: 1rem !important; | ||||
|             font-weight: normal !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:focus { | ||||
|         .p-menuitem-link:focus, | ||||
|         .p-panelmenu-header > a:focus, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:hover { | ||||
|         .p-menuitem-link:hover, | ||||
|         .p-panelmenu-header > a:hover, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:hover { | ||||
|             background-color: $secondaryBackgroundColor !important; | ||||
|             $border-radius: 20px; | ||||
|             border-radius: $border-radius 0px 0px $border-radius; | ||||
|              | ||||
|  | ||||
|             .p-menuitem-text, | ||||
|             .p-menuitem-icon { | ||||
|             .p-menuitem-icon, | ||||
|             .p-menuitem-text, | ||||
|             .p-panelmenu-icon { | ||||
|                 color: $primaryHeaderColor !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .p-panelmenu-content { | ||||
|             margin: 5px 0px 5px 10px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .p-menu-overlay { | ||||
| @@ -386,6 +425,9 @@ | ||||
|  | ||||
|     input, | ||||
|     .p-password { | ||||
|         border-radius: 10px; | ||||
|         border: $default-border; | ||||
|  | ||||
|         &:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
| @@ -430,7 +472,7 @@ | ||||
|         color: $primaryTextColor !important; | ||||
|         border: 0 !important; | ||||
|         padding: 0px !important; | ||||
|          | ||||
|  | ||||
|         &:hover { | ||||
|             background-color: transparent !important; | ||||
|             color: $primaryHeaderColor !important; | ||||
|   | ||||
| @@ -15,16 +15,9 @@ | ||||
|     $primaryErrorColor: #b00020; | ||||
|     $secondaryErrorColor: #e94948; | ||||
|  | ||||
|     $default-border: 1px solid $secondaryBackgroundColor3; | ||||
|     $default-border: 2px solid $secondaryBackgroundColor3; | ||||
|  | ||||
|     background-color: $primaryBackgroundColor !important; | ||||
|  | ||||
|     html, | ||||
|     body { | ||||
|         margin: 0; | ||||
|  | ||||
|         font-size: 16px; | ||||
|     } | ||||
|     background-color: $primaryBackgroundColor; | ||||
|  | ||||
|     h1, | ||||
|     h2 { | ||||
| @@ -122,6 +115,35 @@ | ||||
|                         .content-divider { | ||||
|                             border-bottom: $default-border; | ||||
|                         } | ||||
|  | ||||
|                         .server-list-wrapper { | ||||
|                             .server-filter { | ||||
|                             } | ||||
|  | ||||
|                             .server-count { | ||||
|                             } | ||||
|  | ||||
|                             .server-list { | ||||
|                                 .server { | ||||
|                                     border: $default-border; | ||||
|                                     border-radius: 15px; | ||||
|  | ||||
|                                     .logo { | ||||
|                                         img { | ||||
|                                             border-radius: 100%; | ||||
|                                         } | ||||
|                                     } | ||||
|  | ||||
|                                     .name { | ||||
|                                         color: $primaryHeaderColor; | ||||
|                                     } | ||||
|  | ||||
|                                     &:hover { | ||||
|                                         border-color: $primaryHeaderColor !important; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -230,28 +252,43 @@ | ||||
|         stroke: $primaryHeaderColor !important; | ||||
|     } | ||||
|  | ||||
|     .p-menu { | ||||
|     .p-menu, | ||||
|     .p-panelmenu { | ||||
|         color: $primaryTextColor !important; | ||||
|  | ||||
|         .p-menuitem-link .p-menuitem-text, | ||||
|         .p-menuitem-link .p-menuitem-icon { | ||||
|         .p-menuitem-link .p-menuitem-icon, | ||||
|         .p-panelmenu-header > a { | ||||
|             color: $primaryTextColor !important; | ||||
|             background: transparent !important; | ||||
|             font-size: 1rem !important; | ||||
|             font-weight: normal !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:focus { | ||||
|         .p-menuitem-link:focus, | ||||
|         .p-panelmenu-header > a:focus, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:hover { | ||||
|         .p-menuitem-link:hover, | ||||
|         .p-panelmenu-header > a:hover, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:hover { | ||||
|             background-color: $secondaryBackgroundColor !important; | ||||
|             $border-radius: 20px; | ||||
|             border-radius: $border-radius 0px 0px $border-radius; | ||||
|  | ||||
|             .p-menuitem-text, | ||||
|             .p-menuitem-icon { | ||||
|             .p-menuitem-icon, | ||||
|             .p-menuitem-text, | ||||
|             .p-panelmenu-icon { | ||||
|                 color: $primaryHeaderColor !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .p-panelmenu-content { | ||||
|             margin: 5px 0px 5px 10px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .p-menu-overlay { | ||||
| @@ -390,6 +427,9 @@ | ||||
|  | ||||
|     input, | ||||
|     .p-password { | ||||
|         border-radius: 10px; | ||||
|         border: $default-border; | ||||
|  | ||||
|         &:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
| @@ -434,7 +474,7 @@ | ||||
|         color: $primaryTextColor !important; | ||||
|         border: 0 !important; | ||||
|         padding: 0px !important; | ||||
|          | ||||
|  | ||||
|         &:hover { | ||||
|             background-color: transparent !important; | ||||
|             color: $primaryHeaderColor !important; | ||||
|   | ||||
| @@ -15,14 +15,9 @@ | ||||
|     $primaryErrorColor: #b00020; | ||||
|     $secondaryErrorColor: #e94948; | ||||
|  | ||||
|     $default-border: 1px solid $secondaryBackgroundColor; | ||||
|     $default-border: 2px solid $secondaryBackgroundColor; | ||||
|  | ||||
|     html, | ||||
|     body { | ||||
|         margin: 0; | ||||
|  | ||||
|         font-size: 16px; | ||||
|     } | ||||
|     background-color: $primaryBackgroundColor; | ||||
|  | ||||
|     h1, | ||||
|     h2 { | ||||
| @@ -121,6 +116,35 @@ | ||||
|                             border-bottom: $default-border; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     .server-list-wrapper { | ||||
|                         .server-filter { | ||||
|                         } | ||||
|  | ||||
|                         .server-count { | ||||
|                         } | ||||
|  | ||||
|                         .server-list { | ||||
|                             .server { | ||||
|                                 border: $default-border; | ||||
|                                 border-radius: 15px; | ||||
|  | ||||
|                                 .logo { | ||||
|                                     img { | ||||
|                                         border-radius: 100%; | ||||
|                                     } | ||||
|                                 } | ||||
|  | ||||
|                                 .name { | ||||
|                                     color: $primaryHeaderColor; | ||||
|                                 } | ||||
|  | ||||
|                                 &:hover { | ||||
|                                     border-color: $primaryHeaderColor !important; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -226,28 +250,43 @@ | ||||
|         stroke: $primaryHeaderColor !important; | ||||
|     } | ||||
|  | ||||
|     .p-menu { | ||||
|     .p-menu, | ||||
|     .p-panelmenu { | ||||
|         color: $primaryTextColor !important; | ||||
|  | ||||
|         .p-menuitem-link .p-menuitem-text, | ||||
|         .p-menuitem-link .p-menuitem-icon { | ||||
|         .p-menuitem-link .p-menuitem-icon, | ||||
|         .p-panelmenu-header > a { | ||||
|             color: $primaryTextColor !important; | ||||
|             background: transparent !important; | ||||
|             font-size: 1rem !important; | ||||
|             font-weight: normal !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:focus { | ||||
|         .p-menuitem-link:focus, | ||||
|         .p-panelmenu-header > a:focus, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
|  | ||||
|         .p-menuitem-link:hover { | ||||
|         .p-menuitem-link:hover, | ||||
|         .p-panelmenu-header > a:hover, | ||||
|         .p-panelmenu-content .p-menuitem .p-menuitem-link:hover { | ||||
|             background-color: $secondaryBackgroundColor !important; | ||||
|             $border-radius: 20px; | ||||
|             border-radius: $border-radius 0px 0px $border-radius; | ||||
|  | ||||
|             .p-menuitem-text, | ||||
|             .p-menuitem-icon { | ||||
|             .p-menuitem-icon, | ||||
|             .p-menuitem-text, | ||||
|             .p-panelmenu-icon { | ||||
|                 color: $primaryHeaderColor !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .p-panelmenu-content { | ||||
|             margin: 5px 0px 5px 10px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .p-menu-overlay { | ||||
| @@ -386,6 +425,9 @@ | ||||
|  | ||||
|     input, | ||||
|     .p-password { | ||||
|         border-radius: 10px; | ||||
|         border: $default-border; | ||||
|  | ||||
|         &:focus { | ||||
|             box-shadow: none !important; | ||||
|         } | ||||
| @@ -430,7 +472,7 @@ | ||||
|         color: $primaryTextColor !important; | ||||
|         border: 0 !important; | ||||
|         padding: 0px !important; | ||||
|          | ||||
|  | ||||
|         &:hover { | ||||
|             background-color: transparent !important; | ||||
|             color: $primaryHeaderColor !important; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user