diff --git a/kdb-bot/src/bot/config/feature-flags.json b/kdb-bot/src/bot/config/feature-flags.json index 2d214f7f3e..68bf6e002c 100644 --- a/kdb-bot/src/bot/config/feature-flags.json +++ b/kdb-bot/src/bot/config/feature-flags.json @@ -10,7 +10,6 @@ "DatabaseModule": true, "ModeratorModule": true, "PermissionModule": true, - "PresenceModule": true, - "ApiOnly": true + "PresenceModule": true } } diff --git a/kdb-bot/src/bot_api/abc/auth_service_abc.py b/kdb-bot/src/bot_api/abc/auth_service_abc.py index 3a006a0dcf..cd54128dda 100644 --- a/kdb-bot/src/bot_api/abc/auth_service_abc.py +++ b/kdb-bot/src/bot_api/abc/auth_service_abc.py @@ -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 diff --git a/kdb-bot/src/bot_api/api_module.py b/kdb-bot/src/bot_api/api_module.py index d6d659ecba..0a9446ad5d 100644 --- a/kdb-bot/src/bot_api/api_module.py +++ b/kdb-bot/src/bot_api/api_module.py @@ -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) diff --git a/kdb-bot/src/bot_api/controller/auth_controller.py b/kdb-bot/src/bot_api/controller/auth_controller.py index a23d490dff..78a17e79e6 100644 --- a/kdb-bot/src/bot_api/controller/auth_controller.py +++ b/kdb-bot/src/bot_api/controller/auth_controller.py @@ -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/') - @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/') - @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/') - @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 diff --git a/kdb-bot/src/bot_api/controller/discord/__init__.py b/kdb-bot/src/bot_api/controller/discord/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kdb-bot/src/bot_api/controller/discord/server_controller.py b/kdb-bot/src/bot_api/controller/discord/server_controller.py new file mode 100644 index 0000000000..1c362ab957 --- /dev/null +++ b/kdb-bot/src/bot_api/controller/discord/server_controller.py @@ -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/') + @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()) diff --git a/kdb-bot/src/bot_api/event/__init__.py b/kdb-bot/src/bot_api/event/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kdb-bot/src/bot_api/exception/service_error_code_enum.py b/kdb-bot/src/bot_api/exception/service_error_code_enum.py index 7b19aeb18c..5848bf3374 100644 --- a/kdb-bot/src/bot_api/exception/service_error_code_enum.py +++ b/kdb-bot/src/bot_api/exception/service_error_code_enum.py @@ -21,3 +21,4 @@ class ServiceErrorCode(Enum): MailError = 10 Unauthorized = 11 + Forbidden = 12 diff --git a/kdb-bot/src/bot_api/filter/discord/__init__.py b/kdb-bot/src/bot_api/filter/discord/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kdb-bot/src/bot_api/filter/discord/server_select_criteria.py b/kdb-bot/src/bot_api/filter/discord/server_select_criteria.py new file mode 100644 index 0000000000..f1454a8c95 --- /dev/null +++ b/kdb-bot/src/bot_api/filter/discord/server_select_criteria.py @@ -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 diff --git a/kdb-bot/src/bot_api/model/auth_user_dto.py b/kdb-bot/src/bot_api/model/auth_user_dto.py index 4fb5339455..8d42e1fad6 100644 --- a/kdb-bot/src/bot_api/model/auth_user_dto.py +++ b/kdb-bot/src/bot_api/model/auth_user_dto.py @@ -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, } diff --git a/kdb-bot/src/bot_api/model/discord/__init__.py b/kdb-bot/src/bot_api/model/discord/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kdb-bot/src/bot_api/model/discord/server_dto.py b/kdb-bot/src/bot_api/model/discord/server_dto.py new file mode 100644 index 0000000000..07cd4365c4 --- /dev/null +++ b/kdb-bot/src/bot_api/model/discord/server_dto.py @@ -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, + } diff --git a/kdb-bot/src/bot_api/model/discord/server_filtered_result_dto.py b/kdb-bot/src/bot_api/model/discord/server_filtered_result_dto.py new file mode 100644 index 0000000000..eceb642ca0 --- /dev/null +++ b/kdb-bot/src/bot_api/model/discord/server_filtered_result_dto.py @@ -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 + } diff --git a/kdb-bot/src/bot_api/route/route.py b/kdb-bot/src/bot_api/route/route.py index ce8826bcd1..26bdad2b80 100644 --- a/kdb-bot/src/bot_api/route/route.py +++ b/kdb-bot/src/bot_api/route/route.py @@ -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 diff --git a/kdb-bot/src/bot_api/service/auth_service.py b/kdb-bot/src/bot_api/service/auth_service.py index e1e4a2b943..b2056530d7 100644 --- a/kdb-bot/src/bot_api/service/auth_service.py +++ b/kdb-bot/src/bot_api/service/auth_service.py @@ -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 diff --git a/kdb-bot/src/bot_api/service/discord_service.py b/kdb-bot/src/bot_api/service/discord_service.py new file mode 100644 index 0000000000..304e340a95 --- /dev/null +++ b/kdb-bot/src/bot_api/service/discord_service.py @@ -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 diff --git a/kdb-bot/src/bot_api/transformer/auth_user_transformer.py b/kdb-bot/src/bot_api/transformer/auth_user_transformer.py index 3f84a9399a..b5608e2376 100644 --- a/kdb-bot/src/bot_api/transformer/auth_user_transformer.py +++ b/kdb-bot/src/bot_api/transformer/auth_user_transformer.py @@ -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 ) diff --git a/kdb-bot/src/bot_api/transformer/server_transformer.py b/kdb-bot/src/bot_api/transformer/server_transformer.py new file mode 100644 index 0000000000..d883c42d5d --- /dev/null +++ b/kdb-bot/src/bot_api/transformer/server_transformer.py @@ -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, + ) diff --git a/kdb-bot/src/bot_data/abc/server_repository_abc.py b/kdb-bot/src/bot_data/abc/server_repository_abc.py index d1ab13fe63..f3eea75a9d 100644 --- a/kdb-bot/src/bot_data/abc/server_repository_abc.py +++ b/kdb-bot/src/bot_data/abc/server_repository_abc.py @@ -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 diff --git a/kdb-bot/src/bot_data/abc/user_repository_abc.py b/kdb-bot/src/bot_data/abc/user_repository_abc.py index ff7f625445..12878b6fd6 100644 --- a/kdb-bot/src/bot_data/abc/user_repository_abc.py +++ b/kdb-bot/src/bot_data/abc/user_repository_abc.py @@ -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 diff --git a/kdb-bot/src/bot_data/migration/api_migration.py b/kdb-bot/src/bot_data/migration/api_migration.py index 80754c88a5..6860ae1440 100644 --- a/kdb-bot/src/bot_data/migration/api_migration.py +++ b/kdb-bot/src/bot_data/migration/api_migration.py @@ -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`) ) """) ) diff --git a/kdb-bot/src/bot_data/model/auth_user.py b/kdb-bot/src/bot_data/model/auth_user.py index 26ba09a868..fe92ad1eb9 100644 --- a/kdb-bot/src/bot_data/model/auth_user.py +++ b/kdb-bot/src/bot_data/model/auth_user.py @@ -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}; """) diff --git a/kdb-bot/src/bot_data/service/auth_user_repository_service.py b/kdb-bot/src/bot_data/service/auth_user_repository_service.py index 4918b26980..c8fee7b4aa 100644 --- a/kdb-bot/src/bot_data/service/auth_user_repository_service.py +++ b/kdb-bot/src/bot_data/service/auth_user_repository_service.py @@ -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]) ) diff --git a/kdb-bot/src/bot_data/service/server_repository_service.py b/kdb-bot/src/bot_data/service/server_repository_service.py index 728d2bc679..210a6dee95 100644 --- a/kdb-bot/src/bot_data/service/server_repository_service.py +++ b/kdb-bot/src/bot_data/service/server_repository_service.py @@ -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] diff --git a/kdb-bot/src/bot_data/service/user_repository_service.py b/kdb-bot/src/bot_data/service/user_repository_service.py index f2543e5dd7..83f9862778 100644 --- a/kdb-bot/src/bot_data/service/user_repository_service.py +++ b/kdb-bot/src/bot_data/service/user_repository_service.py @@ -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) diff --git a/kdb-web/.prettierrc b/kdb-web/.prettierrc new file mode 100644 index 0000000000..5a938ce18e --- /dev/null +++ b/kdb-web/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "useTabs": false +} diff --git a/kdb-web/src/app/app-routing.module.ts b/kdb-web/src/app/app-routing.module.ts index 803deb705b..1a1c9c3b46 100644 --- a/kdb-web/src/app/app-routing.module.ts +++ b/kdb-web/src/app/app-routing.module.ts @@ -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) }, diff --git a/kdb-web/src/app/components/sidebar/sidebar.component.html b/kdb-web/src/app/components/sidebar/sidebar.component.html index b3bb4f3ba9..6f72d17e6c 100644 --- a/kdb-web/src/app/components/sidebar/sidebar.component.html +++ b/kdb-web/src/app/components/sidebar/sidebar.component.html @@ -1,3 +1,3 @@ \ No newline at end of file diff --git a/kdb-web/src/app/components/sidebar/sidebar.component.ts b/kdb-web/src/app/components/sidebar/sidebar.component.ts index cbf13c6f7c..d43b42e522 100644 --- a/kdb-web/src/app/components/sidebar/sidebar.component.ts +++ b/kdb-web/src/app/components/sidebar/sidebar.component.ts @@ -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' }, + ] + }, + ); + } + } diff --git a/kdb-web/src/app/models/discord/server.dto.ts b/kdb-web/src/app/models/discord/server.dto.ts new file mode 100644 index 0000000000..a97625b50a --- /dev/null +++ b/kdb-web/src/app/models/discord/server.dto.ts @@ -0,0 +1,7 @@ +export interface ServerDTO { + serverId: number; + discordId: number; + name: string; + memberCount: number; + iconURL: string | null; +} \ No newline at end of file diff --git a/kdb-web/src/app/models/selection/logins/login-select-criterion.dto.ts b/kdb-web/src/app/models/selection/logins/login-select-criterion.dto.ts deleted file mode 100644 index e1998d60bc..0000000000 --- a/kdb-web/src/app/models/selection/logins/login-select-criterion.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SelectCriterion } from "../select-criterion.model"; - -export interface LoginSelectCriterion extends SelectCriterion { - timeFrom: string; - timeTo: string; - userName: string; - hostName: string; -} \ No newline at end of file diff --git a/kdb-web/src/app/models/selection/server/get-filtered-servers-result.dto.ts b/kdb-web/src/app/models/selection/server/get-filtered-servers-result.dto.ts new file mode 100644 index 0000000000..4dfe801a2b --- /dev/null +++ b/kdb-web/src/app/models/selection/server/get-filtered-servers-result.dto.ts @@ -0,0 +1,7 @@ +import { AuthUserDTO } from "../../auth/auth-user.dto"; +import { ServerDTO } from "../../discord/server.dto"; + +export interface GetFilteredServersResultDTO { + servers: ServerDTO[]; + totalCount: number; +} \ No newline at end of file diff --git a/kdb-web/src/app/models/selection/server/server-select-criterion.dto.ts b/kdb-web/src/app/models/selection/server/server-select-criterion.dto.ts new file mode 100644 index 0000000000..08b9ea5925 --- /dev/null +++ b/kdb-web/src/app/models/selection/server/server-select-criterion.dto.ts @@ -0,0 +1,5 @@ +import { SelectCriterion } from "../select-criterion.model"; + +export interface ServerSelectCriterion extends SelectCriterion { + name: string | null; +} \ No newline at end of file diff --git a/kdb-web/src/app/modules/admin/auth-users/components/auth-user/auth-user.component.ts b/kdb-web/src/app/modules/admin/auth-users/components/auth-user/auth-user.component.ts index c972e7f215..a9243563b6 100644 --- a/kdb-web/src/app/modules/admin/auth-users/components/auth-user/auth-user.component.ts +++ b/kdb-web/src/app/modules/admin/auth-users/components/auth-user/auth-user.component.ts @@ -85,7 +85,7 @@ export class AuthUserComponent implements OnInit { }; this.setFilterForm(); - // this.loadNextPage(); + this.loadNextPage(); } setFilterForm() { diff --git a/kdb-web/src/app/modules/shared/shared.module.ts b/kdb-web/src/app/modules/shared/shared.module.ts index 9a77a513cc..59c0d0ef06 100644 --- a/kdb-web/src/app/modules/shared/shared.module.ts +++ b/kdb-web/src/app/modules/shared/shared.module.ts @@ -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, diff --git a/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.html b/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.html index 04e795ec56..ded533101a 100644 --- a/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.html +++ b/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.html @@ -1,3 +1,50 @@ -

dashboard works!

+

+ {{'view.dashboard.header' | translate}} +

+
+
+

+ + {{'view.dashboard.server.header' | translate}} +

+
-
+
+
+
+
+
+ +
+
+
+ +
+ {{servers.length}} {{'view.dashboard.of' | translate}} {{totalRecords}} {{'view.dashboard.servers' | translate}}: +
+
+ +
+
+ + +
+

+ {{server.name}} +

+ +
+ + {{server.memberCount}} + {{'view.dashboard.server.member_count' | translate}} +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.ts b/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.ts index 25d48a7ae5..046e00c618 100644 --- a/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.ts +++ b/kdb-web/src/app/modules/view/dashboard/components/dashboard/dashboard.component.ts @@ -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, + }>; + + 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']); + } } diff --git a/kdb-web/src/app/modules/view/dashboard/dashboard-routing.module.ts b/kdb-web/src/app/modules/view/dashboard/dashboard-routing.module.ts index 27f772462c..37add74128 100644 --- a/kdb-web/src/app/modules/view/dashboard/dashboard-routing.module.ts +++ b/kdb-web/src/app/modules/view/dashboard/dashboard-routing.module.ts @@ -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({ diff --git a/kdb-web/src/app/modules/view/dashboard/dashboard.module.ts b/kdb-web/src/app/modules/view/dashboard/dashboard.module.ts index 0b3abe11d6..80438b6ee9 100644 --- a/kdb-web/src/app/modules/view/dashboard/dashboard.module.ts +++ b/kdb-web/src/app/modules/view/dashboard/dashboard.module.ts @@ -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({ diff --git a/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.html b/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.html new file mode 100644 index 0000000000..1fbf61d3c5 --- /dev/null +++ b/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.html @@ -0,0 +1,36 @@ +

+ {{'view.dashboard.header' | translate}} +

+
+
+

+ + {{'view.dashboard.server.header' | translate}} +

+
+ +
+
+
+
+ + +
+

+ {{server.name}} +

+ +
+ + {{server.memberCount}} + {{'view.dashboard.server.member_count' | translate}} +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.scss b/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.spec.ts b/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.spec.ts new file mode 100644 index 0000000000..9f49b43a16 --- /dev/null +++ b/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ServerDashboardComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ServerDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.ts b/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.ts new file mode 100644 index 0000000000..158c771167 --- /dev/null +++ b/kdb-web/src/app/modules/view/server/server-dashboard/server-dashboard.component.ts @@ -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(); + } + +} diff --git a/kdb-web/src/app/modules/view/server/server-routing.module.ts b/kdb-web/src/app/modules/view/server/server-routing.module.ts new file mode 100644 index 0000000000..7b9f3ee70d --- /dev/null +++ b/kdb-web/src/app/modules/view/server/server-routing.module.ts @@ -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 { } diff --git a/kdb-web/src/app/modules/view/server/server.module.ts b/kdb-web/src/app/modules/view/server/server.module.ts new file mode 100644 index 0000000000..824e2412df --- /dev/null +++ b/kdb-web/src/app/modules/view/server/server.module.ts @@ -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 { } diff --git a/kdb-web/src/app/services/data/data.service.ts b/kdb-web/src/app/services/data/data.service.ts index 36cccbb5a4..6b1a0f76a7 100644 --- a/kdb-web/src/app/services/data/data.service.ts +++ b/kdb-web/src/app/services/data/data.service.ts @@ -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> { + return this.http.get>(`${this.appsettings.getApiURL()}/api/discord/server/servers`, { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }); + } + + getAllServersByUser(): Observable> { + return this.http.get>(`${this.appsettings.getApiURL()}/api/discord/server/servers-by-user`, { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }); + } + + getFilteredServers(selectCriterions: ServerSelectCriterion): Observable { + return this.http.post(`${this.appsettings.getApiURL()}/api/discord/server/get/filtered`, selectCriterions, { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }); + } + + getServerByID(id: number): Observable { + return this.http.get(`${this.appsettings.getApiURL()}/api/discord/server/get/${id}`, { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }); + } } diff --git a/kdb-web/src/app/services/data/server.service.spec.ts b/kdb-web/src/app/services/data/server.service.spec.ts new file mode 100644 index 0000000000..906c1601bb --- /dev/null +++ b/kdb-web/src/app/services/data/server.service.spec.ts @@ -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(); + }); +}); diff --git a/kdb-web/src/app/services/data/server.service.ts b/kdb-web/src/app/services/data/server.service.ts new file mode 100644 index 0000000000..05d8673d46 --- /dev/null +++ b/kdb-web/src/app/services/data/server.service.ts @@ -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(null); + + constructor() { + this.server$.subscribe(server => { + if (!server) { + return; + } + this.server = server; + }); + } + + +} diff --git a/kdb-web/src/app/services/theme/theme.service.ts b/kdb-web/src/app/services/theme/theme.service.ts index 914ba774ef..6dc3410ab2 100644 --- a/kdb-web/src/app/services/theme/theme.service.ts +++ b/kdb-web/src/app/services/theme/theme.service.ts @@ -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; diff --git a/kdb-web/src/assets/i18n/de.json b/kdb-web/src/assets/i18n/de.json index 894bd46195..9eec0bf3a4 100644 --- a/kdb-web/src/assets/i18n/de.json +++ b/kdb-web/src/assets/i18n/de.json @@ -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" } -} \ No newline at end of file +} diff --git a/kdb-web/src/styles.scss b/kdb-web/src/styles.scss index ee146861c3..8bbd6cd8a5 100644 --- a/kdb-web/src/styles.scss +++ b/kdb-web/src/styles.scss @@ -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 { + } + } + } + } + } } } } diff --git a/kdb-web/src/styles/primeng-fixes.scss b/kdb-web/src/styles/primeng-fixes.scss index 560ad4e34c..5bad03ee8e 100644 --- a/kdb-web/src/styles/primeng-fixes.scss +++ b/kdb-web/src/styles/primeng-fixes.scss @@ -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 !! */ } diff --git a/kdb-web/src/styles/themes/default-dark-theme.scss b/kdb-web/src/styles/themes/default-dark-theme.scss index 7507a6cb05..8a5b60fae4 100644 --- a/kdb-web/src/styles/themes/default-dark-theme.scss +++ b/kdb-web/src/styles/themes/default-dark-theme.scss @@ -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; diff --git a/kdb-web/src/styles/themes/default-light-theme.scss b/kdb-web/src/styles/themes/default-light-theme.scss index 5993a05808..bc58a6f7a0 100644 --- a/kdb-web/src/styles/themes/default-light-theme.scss +++ b/kdb-web/src/styles/themes/default-light-theme.scss @@ -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; diff --git a/kdb-web/src/styles/themes/sh-edraft-dark-theme.scss b/kdb-web/src/styles/themes/sh-edraft-dark-theme.scss index ee2da9f469..9e45019f04 100644 --- a/kdb-web/src/styles/themes/sh-edraft-dark-theme.scss +++ b/kdb-web/src/styles/themes/sh-edraft-dark-theme.scss @@ -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; diff --git a/kdb-web/src/styles/themes/sh-edraft-light-theme.scss b/kdb-web/src/styles/themes/sh-edraft-light-theme.scss index fdd809491c..26c70cd5d0 100644 --- a/kdb-web/src/styles/themes/sh-edraft-light-theme.scss +++ b/kdb-web/src/styles/themes/sh-edraft-light-theme.scss @@ -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;