Merge pull request '0.3-#70 - Im Web Interface Server auswählen - (#72)' (#73) from #72 into #70

Reviewed-on: sh-edraft.de/kd_discord_bot#73
Closes #72
This commit is contained in:
Sven Heidemann 2022-10-18 16:37:04 +02:00
commit f553779797
57 changed files with 1221 additions and 134 deletions

View File

@ -10,7 +10,6 @@
"DatabaseModule": true, "DatabaseModule": true,
"ModeratorModule": true, "ModeratorModule": true,
"PermissionModule": true, "PermissionModule": true,
"PresenceModule": true, "PresenceModule": true
"ApiOnly": true
} }
} }

View File

@ -1,4 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional
from cpl_query.extension import List from cpl_query.extension import List
@ -23,6 +24,12 @@ class AuthServiceABC(ABC):
@abstractmethod @abstractmethod
def decode_token(self, token: str) -> dict: pass 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 @abstractmethod
async def get_all_auth_users_async(self) -> List[AuthUserDTO]: pass async def get_all_auth_users_async(self) -> List[AuthUserDTO]: pass

View File

@ -11,10 +11,12 @@ from flask import Flask
from bot_api.abc.auth_service_abc import AuthServiceABC from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api from bot_api.api import Api
from bot_api.api_thread import ApiThread 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.gui_controller import GuiController
from bot_api.controller.auth_controller import AuthController from bot_api.controller.auth_controller import AuthController
from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent from bot_api.event.bot_api_on_ready_event import BotApiOnReadyEvent
from bot_api.service.auth_service import AuthService 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.abc.module_abc import ModuleABC
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
@ -41,6 +43,8 @@ class ApiModule(ModuleABC):
services.add_transient(AuthServiceABC, AuthService) services.add_transient(AuthServiceABC, AuthService)
services.add_transient(AuthController) services.add_transient(AuthController)
services.add_transient(GuiController) services.add_transient(GuiController)
services.add_transient(DiscordService)
services.add_transient(ServerController)
# cpl-discord # cpl-discord
self._dc.add_event(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent) self._dc.add_event(DiscordEventTypesEnum.on_ready.value, BotApiOnReadyEvent)

View File

@ -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.token_dto import TokenDTO
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
from bot_api.route.route import Route from bot_api.route.route import Route
from bot_data.model.auth_role_enum import AuthRoleEnum
class AuthController: class AuthController:
@ -41,13 +42,13 @@ class AuthController:
self._auth_service = auth_service self._auth_service = auth_service
@Route.get(f'{BasePath}/users') @Route.get(f'{BasePath}/users')
@Route.authorize @Route.authorize(role=AuthRoleEnum.admin)
async def get_all_users(self) -> Response: async def get_all_users(self) -> Response:
result = await self._auth_service.get_all_auth_users_async() result = await self._auth_service.get_all_auth_users_async()
return jsonify(result.select(lambda x: x.to_dict())) return jsonify(result.select(lambda x: x.to_dict()))
@Route.post(f'{BasePath}/users/get/filtered') @Route.post(f'{BasePath}/users/get/filtered')
@Route.authorize @Route.authorize(role=AuthRoleEnum.admin)
async def get_filtered_users(self) -> Response: async def get_filtered_users(self) -> Response:
dto: AuthUserSelectCriteria = JSONProcessor.process(AuthUserSelectCriteria, request.get_json(force=True, silent=True)) dto: AuthUserSelectCriteria = JSONProcessor.process(AuthUserSelectCriteria, request.get_json(force=True, silent=True))
result = await self._auth_service.get_filtered_auth_users_async(dto) result = await self._auth_service.get_filtered_auth_users_async(dto)
@ -55,13 +56,13 @@ class AuthController:
return jsonify(result.to_dict()) return jsonify(result.to_dict())
@Route.get(f'{BasePath}/users/get/<email>') @Route.get(f'{BasePath}/users/get/<email>')
@Route.authorize @Route.authorize(role=AuthRoleEnum.admin)
async def get_user_from_email(self, email: str) -> Response: async def get_user_from_email(self, email: str) -> Response:
result = await self._auth_service.get_auth_user_by_email_async(email) result = await self._auth_service.get_auth_user_by_email_async(email)
return jsonify(result.to_dict()) return jsonify(result.to_dict())
@Route.get(f'{BasePath}/users/find/<email>') @Route.get(f'{BasePath}/users/find/<email>')
@Route.authorize @Route.authorize(role=AuthRoleEnum.admin)
async def find_user_from_email(self, email: str) -> Response: async def find_user_from_email(self, email: str) -> Response:
result = await self._auth_service.find_auth_user_by_email_async(email) result = await self._auth_service.find_auth_user_by_email_async(email)
return jsonify(result.to_dict()) return jsonify(result.to_dict())
@ -109,7 +110,7 @@ class AuthController:
return '', 200 return '', 200
@Route.post(f'{BasePath}/update-user-as-admin') @Route.post(f'{BasePath}/update-user-as-admin')
@Route.authorize @Route.authorize(role=AuthRoleEnum.admin)
async def update_user_as_admin(self): async def update_user_as_admin(self):
dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True)) dto: UpdateAuthUserDTO = JSONProcessor.process(UpdateAuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.update_user_as_admin_async(dto) await self._auth_service.update_user_as_admin_async(dto)
@ -129,14 +130,14 @@ class AuthController:
return '', 200 return '', 200
@Route.post(f'{BasePath}/delete-user') @Route.post(f'{BasePath}/delete-user')
@Route.authorize @Route.authorize(role=AuthRoleEnum.admin)
async def delete_user(self): async def delete_user(self):
dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True)) dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True))
await self._auth_service.delete_auth_user_async(dto) await self._auth_service.delete_auth_user_async(dto)
return '', 200 return '', 200
@Route.post(f'{BasePath}/delete-user-by-mail/<email>') @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): async def delete_user_by_mail(self, email: str):
await self._auth_service.delete_auth_user_by_email_async(email) await self._auth_service.delete_auth_user_by_email_async(email)
return '', 200 return '', 200

View 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())

View File

View File

@ -21,3 +21,4 @@ class ServiceErrorCode(Enum):
MailError = 10 MailError = 10
Unauthorized = 11 Unauthorized = 11
Forbidden = 12

View File

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

View File

@ -15,6 +15,7 @@ class AuthUserDTO(DtoABC):
password: str, password: str,
confirmation_id: Optional[str], confirmation_id: Optional[str],
auth_role: AuthRoleEnum, auth_role: AuthRoleEnum,
user_id: Optional[int],
): ):
DtoABC.__init__(self) DtoABC.__init__(self)
@ -25,6 +26,7 @@ class AuthUserDTO(DtoABC):
self._password = password self._password = password
self._is_confirmed = confirmation_id is None self._is_confirmed = confirmation_id is None
self._auth_role = auth_role self._auth_role = auth_role
self._user_id = user_id
@property @property
def id(self) -> int: def id(self) -> int:
@ -78,6 +80,14 @@ class AuthUserDTO(DtoABC):
def auth_role(self, value: AuthRoleEnum): def auth_role(self, value: AuthRoleEnum):
self._auth_role = value 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): def from_dict(self, values: dict):
self._id = values['id'] self._id = values['id']
self._first_name = values['firstName'] self._first_name = values['firstName']
@ -86,6 +96,7 @@ class AuthUserDTO(DtoABC):
self._password = values['password'] self._password = values['password']
self._is_confirmed = values['isConfirmed'] self._is_confirmed = values['isConfirmed']
self._auth_role = values['authRole'] self._auth_role = values['authRole']
self._user_id = values['userId']
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
@ -96,4 +107,5 @@ class AuthUserDTO(DtoABC):
'password': self._password, 'password': self._password,
'isConfirmed': self._is_confirmed, 'isConfirmed': self._is_confirmed,
'authRole': self._auth_role.value, 'authRole': self._auth_role.value,
'userId': self._user_id,
} }

View 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,
}

View File

@ -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
}

View File

@ -1,5 +1,6 @@
import functools
from functools import wraps from functools import wraps
from typing import Optional from typing import Optional, Callable
from flask import request, jsonify from flask import request, jsonify
from flask_cors import cross_origin 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.exception.service_exception import ServiceException
from bot_api.model.error_dto import ErrorDTO from bot_api.model.error_dto import ErrorDTO
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.model.auth_role_enum import AuthRoleEnum
class Route: class Route:
@ -23,7 +25,10 @@ class Route:
cls._auth = auth cls._auth = auth
@classmethod @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) @wraps(f)
async def decorator(*args, **kwargs): async def decorator(*args, **kwargs):
token = None token = None
@ -46,6 +51,23 @@ class Route:
error = ErrorDTO(ex.error_code, ex.message) error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 401 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 await f(*args, **kwargs)
return decorator return decorator

View File

@ -9,6 +9,7 @@ from cpl_core.database.context import DatabaseContextABC
from cpl_core.mailing import EMailClientABC, EMail from cpl_core.mailing import EMailClientABC, EMail
from cpl_query.extension import List from cpl_query.extension import List
from cpl_translation import TranslatePipe from cpl_translation import TranslatePipe
from flask import request
from bot_api.abc.auth_service_abc import AuthServiceABC from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.configuration.authentication_settings import AuthenticationSettings 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.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.model.auth_user import AuthUser 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): class AuthService(AuthServiceABC):
@ -65,7 +68,7 @@ class AuthService(AuthServiceABC):
@staticmethod @staticmethod
def _is_email_valid(email: str) -> bool: 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 True
return False return False
@ -94,6 +97,37 @@ class AuthService(AuthServiceABC):
algorithms=['HS256'] 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: def _create_and_save_refresh_token(self, user: AuthUser) -> str:
token = str(uuid.uuid4()) token = str(uuid.uuid4())
user.refresh_token = token user.refresh_token = token

View 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

View File

@ -9,7 +9,7 @@ from bot_data.model.auth_user import AuthUser
class AuthUserTransformer(TransformerABC): class AuthUserTransformer(TransformerABC):
@staticmethod @staticmethod
def to_db(dto: AuthUser) -> AuthUser: def to_db(dto: AuthUserDTO) -> AuthUser:
return AuthUser( return AuthUser(
dto.first_name, dto.first_name,
dto.last_name, dto.last_name,
@ -19,7 +19,8 @@ class AuthUserTransformer(TransformerABC):
None, None,
None, None,
datetime.now(tz=timezone.utc), 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 id=0 if dto.id is None else dto.id
) )
@ -32,5 +33,6 @@ class AuthUserTransformer(TransformerABC):
db.email, db.email,
db.password, db.password,
db.confirmation_id, db.confirmation_id,
db.auth_role db.auth_role,
db.user_id
) )

View 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,
)

View File

@ -3,6 +3,8 @@ from typing import Optional
from cpl_query.extension import List 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 from bot_data.model.server import Server
@ -14,6 +16,9 @@ class ServerRepositoryABC(ABC):
@abstractmethod @abstractmethod
def get_servers(self) -> List[Server]: pass def get_servers(self) -> List[Server]: pass
@abstractmethod
def get_filtered_servers(self, criteria: ServerSelectCriteria) -> FilteredResult: pass
@abstractmethod @abstractmethod
def get_server_by_id(self, id: int) -> Server: pass def get_server_by_id(self, id: int) -> Server: pass

View File

@ -17,6 +17,9 @@ class UserRepositoryABC(ABC):
@abstractmethod @abstractmethod
def get_user_by_id(self, id: int) -> User: pass def get_user_by_id(self, id: int) -> User: pass
@abstractmethod
def find_user_by_id(self, id: int) -> Optional[User]: pass
@abstractmethod @abstractmethod
def get_users_by_discord_id(self, discord_id: int) -> List[User]: pass def get_users_by_discord_id(self, discord_id: int) -> List[User]: pass

View File

@ -28,9 +28,11 @@ class ApiMigration(MigrationABC):
`ForgotPasswordId` VARCHAR(255) DEFAULT NULL, `ForgotPasswordId` VARCHAR(255) DEFAULT NULL,
`RefreshTokenExpiryTime` DATETIME(6) NOT NULL, `RefreshTokenExpiryTime` DATETIME(6) NOT NULL,
`AuthRole` INT NOT NULL DEFAULT '0', `AuthRole` INT NOT NULL DEFAULT '0',
`UserId` BIGINT NOT NULL DEFAULT '0',
`CreatedOn` DATETIME(6) NOT NULL, `CreatedOn` DATETIME(6) NOT NULL,
`LastModifiedOn` DATETIME(6) NOT NULL, `LastModifiedOn` DATETIME(6) NOT NULL,
PRIMARY KEY(`Id`) PRIMARY KEY(`Id`),
FOREIGN KEY (`UserId`) REFERENCES `Users`(`UserId`)
) )
""") """)
) )

View File

@ -19,6 +19,7 @@ class AuthUser(TableABC):
forgot_password_id: Optional[str], forgot_password_id: Optional[str],
refresh_token_expire_time: datetime, refresh_token_expire_time: datetime,
auth_role: AuthRoleEnum, auth_role: AuthRoleEnum,
user_id: Optional[int],
created_at: datetime = None, created_at: datetime = None,
modified_at: datetime = None, modified_at: datetime = None,
id=0 id=0
@ -34,6 +35,7 @@ class AuthUser(TableABC):
self._refresh_token_expire_time = refresh_token_expire_time self._refresh_token_expire_time = refresh_token_expire_time
self._auth_role_id = auth_role self._auth_role_id = auth_role
self._user_id = user_id
TableABC.__init__(self) TableABC.__init__(self)
self._created_at = created_at if created_at is not None else self._created_at 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): def auth_role(self, value: AuthRoleEnum):
self._auth_role_id = value 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 @staticmethod
def get_select_all_string() -> str: def get_select_all_string() -> str:
return str(f""" return str(f"""
@ -163,6 +173,7 @@ class AuthUser(TableABC):
`ForgotPasswordId`, `ForgotPasswordId`,
`RefreshTokenExpiryTime`, `RefreshTokenExpiryTime`,
`AuthRole`, `AuthRole`,
`UserId`,
`CreatedOn`, `CreatedOn`,
`LastModifiedOn` `LastModifiedOn`
) VALUES ( ) VALUES (
@ -176,6 +187,7 @@ class AuthUser(TableABC):
'{"NULL" if self._forgot_password_id is None else self._forgot_password_id}', '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}',
'{self._refresh_token_expire_time}', '{self._refresh_token_expire_time}',
{self._auth_role_id.value}, {self._auth_role_id.value},
{"NULL" if self._user_id is None else self._user_id}
'{self._created_at}', '{self._created_at}',
'{self._modified_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}', `ForgotPasswordId` = '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}',
`RefreshTokenExpiryTime` = '{self._refresh_token_expire_time}', `RefreshTokenExpiryTime` = '{self._refresh_token_expire_time}',
`AuthRole` = {self._auth_role_id.value}, `AuthRole` = {self._auth_role_id.value},
`UserId` = {"NULL" if self._user_id is None else self._user_id},
`LastModifiedOn` = '{self._modified_at}' `LastModifiedOn` = '{self._modified_at}'
WHERE `AuthUsers`.`Id` = {self._auth_user_id}; WHERE `AuthUsers`.`Id` = {self._auth_user_id};
""") """)

View File

@ -37,6 +37,7 @@ class AuthUserRepositoryService(AuthUserRepositoryABC):
self._get_value_from_result(result[7]), self._get_value_from_result(result[7]),
self._get_value_from_result(result[8]), self._get_value_from_result(result[8]),
AuthRoleEnum(self._get_value_from_result(result[9])), AuthRoleEnum(self._get_value_from_result(result[9])),
self._get_value_from_result(result[10]),
id=self._get_value_from_result(result[0]) id=self._get_value_from_result(result[0])
) )

View File

@ -3,8 +3,10 @@ from typing import Optional
from cpl_core.database.context import DatabaseContextABC from cpl_core.database.context import DatabaseContextABC
from cpl_query.extension import List 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_core.logging.database_logger import DatabaseLogger
from bot_data.abc.server_repository_abc import ServerRepositoryABC from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.filtered_result import FilteredResult
from bot_data.model.server import Server from bot_data.model.server import Server
@ -28,6 +30,26 @@ class ServerRepositoryService(ServerRepositoryABC):
return servers 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: 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)}') 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] result = self._context.select(Server.get_select_by_id_string(server_id))[0]

View File

@ -45,6 +45,21 @@ class UserRepositoryService(UserRepositoryABC):
id=result[0] 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]: def get_users_by_discord_id(self, discord_id: int) -> List[User]:
users = List(User) users = List(User)
self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_discord_id_string(discord_id)}') self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_discord_id_string(discord_id)}')

4
kdb-web/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"tabWidth": 4,
"useTabs": false
}

View File

@ -7,6 +7,7 @@ import { AuthGuard } from './modules/shared/guards/auth/auth.guard';
const routes: Routes = [ const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', loadChildren: () => import('./modules/view/dashboard/dashboard.module').then(m => m.DashboardModule), canActivate: [AuthGuard] }, { 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: '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: '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) }, { path: 'auth', loadChildren: () => import('./modules/auth/auth.module').then(m => m.AuthModule) },

View File

@ -1,3 +1,3 @@
<div class="menu"> <div class="menu">
<p-menu [model]="menuItems"></p-menu> <p-panelMenu [model]="menuItems"></p-panelMenu>
</div> </div>

View File

@ -1,8 +1,10 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core'; import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { MenuItem } from 'primeng/api'; import { MenuItem } from 'primeng/api';
import { AuthRoles } from 'src/app/models/auth/auth-roles.enum'; import { AuthRoles } from 'src/app/models/auth/auth-roles.enum';
import { AuthService } from 'src/app/services/auth/auth.service'; 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'; import { ThemeService } from 'src/app/services/theme/theme.service';
@Component({ @Component({
@ -13,13 +15,16 @@ import { ThemeService } from 'src/app/services/theme/theme.service';
export class SidebarComponent implements OnInit { export class SidebarComponent implements OnInit {
isSidebarOpen: boolean = true; isSidebarOpen: boolean = true;
menuItems!: MenuItem[]; menuItems!: MenuItem[];
private serverId!: number;
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private translateService: TranslateService, private translateService: TranslateService,
private themeService: ThemeService private themeService: ThemeService,
private route: ActivatedRoute,
private serverService: ServerService
) { ) {
this.themeService.isSidebarOpen$.subscribe(value => { this.themeService.isSidebarOpen$.subscribe(value => {
this.isSidebarOpen = value; this.isSidebarOpen = value;
@ -29,6 +34,15 @@ export class SidebarComponent implements OnInit {
this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.setMenu(); this.setMenu();
}); });
this.serverService.server$.subscribe(server => {
if (!server) {
return;
}
this.serverId = server.serverId;
this.setMenu();
});
} }
ngOnInit(): void { ngOnInit(): void {
@ -39,20 +53,40 @@ export class SidebarComponent implements OnInit {
this.authService.hasUserPermission(AuthRoles.Admin).then(hasPermission => { this.authService.hasUserPermission(AuthRoles.Admin).then(hasPermission => {
this.menuItems = []; this.menuItems = [];
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) { if (this.serverId) {
return; this.addServerMenu();
} }
this.menuItems.push( if (hasPermission) {
{ separator: true }, this.addAdminMenu();
{ 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' },
);
this.menuItems = this.menuItems.slice(); 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' },
]
},
);
}
} }

View File

@ -0,0 +1,7 @@
export interface ServerDTO {
serverId: number;
discordId: number;
name: string;
memberCount: number;
iconURL: string | null;
}

View File

@ -1,8 +0,0 @@
import { SelectCriterion } from "../select-criterion.model";
export interface LoginSelectCriterion extends SelectCriterion {
timeFrom: string;
timeTo: string;
userName: string;
hostName: string;
}

View File

@ -0,0 +1,7 @@
import { AuthUserDTO } from "../../auth/auth-user.dto";
import { ServerDTO } from "../../discord/server.dto";
export interface GetFilteredServersResultDTO {
servers: ServerDTO[];
totalCount: number;
}

View File

@ -0,0 +1,5 @@
import { SelectCriterion } from "../select-criterion.model";
export interface ServerSelectCriterion extends SelectCriterion {
name: string | null;
}

View File

@ -85,7 +85,7 @@ export class AuthUserComponent implements OnInit {
}; };
this.setFilterForm(); this.setFilterForm();
// this.loadNextPage(); this.loadNextPage();
} }
setFilterForm() { setFilterForm() {

View File

@ -18,6 +18,7 @@ import { ToastModule } from 'primeng/toast';
import { AuthRolePipe } from './pipes/auth-role.pipe'; import { AuthRolePipe } from './pipes/auth-role.pipe';
import { IpAddressPipe } from './pipes/ip-address.pipe'; import { IpAddressPipe } from './pipes/ip-address.pipe';
import { BoolPipe } from './pipes/bool.pipe'; import { BoolPipe } from './pipes/bool.pipe';
import { PanelMenuModule } from 'primeng/panelmenu';
@ -44,7 +45,8 @@ import { BoolPipe } from './pipes/bool.pipe';
CheckboxModule, CheckboxModule,
DropdownModule, DropdownModule,
TranslateModule, TranslateModule,
DynamicDialogModule DynamicDialogModule,
PanelMenuModule,
], ],
exports: [ exports: [
ButtonModule, ButtonModule,
@ -63,6 +65,7 @@ import { BoolPipe } from './pipes/bool.pipe';
DropdownModule, DropdownModule,
TranslateModule, TranslateModule,
DynamicDialogModule, DynamicDialogModule,
PanelMenuModule,
AuthRolePipe, AuthRolePipe,
IpAddressPipe, IpAddressPipe,
BoolPipe, BoolPipe,

View File

@ -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>

View File

@ -1,4 +1,16 @@
import { Component, OnInit } from '@angular/core'; 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({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -7,8 +19,97 @@ import { Component, OnInit } from '@angular/core';
}) })
export class DashboardComponent implements OnInit { 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']);
}
} }

View File

@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; 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 { SharedModule } from '../../shared/shared.module';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { DashboardRoutingModule } from './dashboard-routing.module';
@NgModule({ @NgModule({

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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();
}
}

View 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 { }

View 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 { }

View File

@ -1,5 +1,9 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; 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'; import { SettingsService } from '../settings/settings.service';
@Injectable({ @Injectable({
@ -11,4 +15,39 @@ export class DataService {
private appsettings: SettingsService, private appsettings: SettingsService,
private http: HttpClient, 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'
})
});
}
} }

View 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();
});
});

View 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;
});
}
}

View File

@ -27,7 +27,7 @@ export class ThemeService {
}); });
this.isSidebarOpen$.subscribe(isSidebarOpen => { this.isSidebarOpen$.subscribe(isSidebarOpen => {
this.isSidebarOpen = isSidebarOpen; this.isSidebarOpen = isSidebarOpen;
this.sidebarWidth$.next(isSidebarOpen ? '150px' : '50px'); this.sidebarWidth$.next(isSidebarOpen ? '175px' : '75px');
}); });
this.sidebarWidth$.subscribe(sidebarWidth => { this.sidebarWidth$.subscribe(sidebarWidth => {
this.sidebarWidth = sidebarWidth; this.sidebarWidth = sidebarWidth;

View File

@ -7,10 +7,11 @@
}, },
"sidebar": { "sidebar": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
"domain_list": "Domänen", "server": "Server",
"host_list": "Rechner", "server_empty": "Kein Server ausgewählt",
"user_list": "Benutzer", "settings": "Einstellungen",
"login_list": "Logins", "members": "Mitglieder",
"administration": "Administration",
"config": "Konfiguration", "config": "Konfiguration",
"auth_user_list": "Benutzer" "auth_user_list": "Benutzer"
}, },
@ -106,7 +107,21 @@
} }
}, },
"view": { "view": {
"dashboard": {}, "dashboard": {
"header": "Dashboard",
"of": "von",
"servers": "Server",
"server": {
"header": "Server",
"member_count": "Mitglid(er)"
},
"filter": {
"name": "Name"
}
},
"server": {
"header": "Server"
},
"user-list": {}, "user-list": {},
"change-password": { "change-password": {
"header": "Passwort ändern", "header": "Passwort ändern",
@ -200,24 +215,8 @@
"Freitag", "Freitag",
"Samstag" "Samstag"
], ],
"dayNamesShort": [ "dayNamesShort": ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam"],
"Son", "dayNamesMin": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
"Mon",
"Die",
"Mit",
"Don",
"Fre",
"Sam"
],
"dayNamesMin": [
"So",
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa"
],
"monthNames": [ "monthNames": [
"Januar", "Januar",
"Februar", "Februar",

View File

@ -10,6 +10,8 @@ body {
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 1rem;
} }
main { main {
@ -18,15 +20,19 @@ main {
min-height: 100vh; min-height: 100vh;
} }
h1, h1 {
h2 {
margin: 0; margin: 0;
font-size: 30px; font-size: 1.75rem;
} }
h2 { h2 {
margin: 0; margin: 0;
font-size: 25px; font-size: 1.5rem;
}
h3 {
margin: 0;
font-size: 1.25rem;
} }
header { header {
@ -207,6 +213,59 @@ header {
.table-header-small-dropdown { .table-header-small-dropdown {
width: 150px; 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 {
}
}
}
}
}
} }
} }
} }

View File

@ -6,14 +6,17 @@
} }
} }
.p-menu { .p-menu,
.p-panelmenu {
background: none !important; background: none !important;
border: none !important; border: none !important;
width: auto !important; width: auto !important;
border-radius: 0px !important; border-radius: 0px !important;
padding: 0 !important; padding: 0 !important;
.p-menuitem-link { .p-menuitem-link,
.p-panelmenu-header > a,
.p-panelmenu-content .p-menuitem .p-menuitem-link {
$distance: 10px; $distance: 10px;
padding: $distance 0px $distance $distance !important; padding: $distance 0px $distance $distance !important;
margin: 4px 0px 4px 6px !important; margin: 4px 0px 4px 6px !important;
@ -24,6 +27,31 @@
top: $headerHeight !important; 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 { ui-menu .ui-menu-parent .ui-menu-child {
width: 400px; /* exagerated !! */ width: 400px; /* exagerated !! */
} }

View File

@ -15,16 +15,9 @@
$primaryErrorColor: #b00020; $primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948; $secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor3; $default-border: 2px solid $secondaryBackgroundColor3;
background-color: $primaryBackgroundColor !important; background-color: $primaryBackgroundColor;
html,
body {
margin: 0;
font-size: 16px;
}
h1, h1,
h2 { h2 {
@ -122,6 +115,35 @@
.content-divider { .content-divider {
border-bottom: $default-border; 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; stroke: $primaryHeaderColor !important;
} }
.p-menu { .p-menu,
.p-panelmenu {
color: $primaryTextColor !important; color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text, .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; 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; 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; background-color: $secondaryBackgroundColor !important;
$border-radius: 20px; $border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius; border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text, .p-menuitem-text,
.p-menuitem-icon { .p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important; color: $primaryHeaderColor !important;
} }
} }
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
} }
.p-menu-overlay { .p-menu-overlay {
@ -388,6 +425,9 @@
input, input,
.p-password { .p-password {
border-radius: 10px;
border: $default-border;
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
} }

View File

@ -15,14 +15,9 @@
$primaryErrorColor: #b00020; $primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948; $secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor; $default-border: 2px solid $secondaryBackgroundColor;
html, background-color: $primaryBackgroundColor;
body {
margin: 0;
font-size: 16px;
}
h1, h1,
h2 { h2 {
@ -120,6 +115,35 @@
.content-divider { .content-divider {
border-bottom: $default-border; 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; stroke: $primaryHeaderColor !important;
} }
.p-menu { .p-menu,
.p-panelmenu {
color: $primaryTextColor !important; color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text, .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; 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; 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; background-color: $secondaryBackgroundColor !important;
$border-radius: 20px; $border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius; border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text, .p-menuitem-text,
.p-menuitem-icon { .p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important; color: $primaryHeaderColor !important;
} }
} }
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
} }
.p-menu-overlay { .p-menu-overlay {
@ -386,6 +425,9 @@
input, input,
.p-password { .p-password {
border-radius: 10px;
border: $default-border;
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
} }

View File

@ -15,16 +15,9 @@
$primaryErrorColor: #b00020; $primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948; $secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor3; $default-border: 2px solid $secondaryBackgroundColor3;
background-color: $primaryBackgroundColor !important; background-color: $primaryBackgroundColor;
html,
body {
margin: 0;
font-size: 16px;
}
h1, h1,
h2 { h2 {
@ -122,6 +115,35 @@
.content-divider { .content-divider {
border-bottom: $default-border; 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; stroke: $primaryHeaderColor !important;
} }
.p-menu { .p-menu,
.p-panelmenu {
color: $primaryTextColor !important; color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text, .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; 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; 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; background-color: $secondaryBackgroundColor !important;
$border-radius: 20px; $border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius; border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text, .p-menuitem-text,
.p-menuitem-icon { .p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important; color: $primaryHeaderColor !important;
} }
} }
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
} }
.p-menu-overlay { .p-menu-overlay {
@ -390,6 +427,9 @@
input, input,
.p-password { .p-password {
border-radius: 10px;
border: $default-border;
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
} }

View File

@ -15,14 +15,9 @@
$primaryErrorColor: #b00020; $primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948; $secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor; $default-border: 2px solid $secondaryBackgroundColor;
html, background-color: $primaryBackgroundColor;
body {
margin: 0;
font-size: 16px;
}
h1, h1,
h2 { h2 {
@ -121,6 +116,35 @@
border-bottom: $default-border; 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; stroke: $primaryHeaderColor !important;
} }
.p-menu { .p-menu,
.p-panelmenu {
color: $primaryTextColor !important; color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text, .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; 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; 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; background-color: $secondaryBackgroundColor !important;
$border-radius: 20px; $border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius; border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text, .p-menuitem-text,
.p-menuitem-icon { .p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important; color: $primaryHeaderColor !important;
} }
} }
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
} }
.p-menu-overlay { .p-menu-overlay {
@ -386,6 +425,9 @@
input, input,
.p-password { .p-password {
border-radius: 10px;
border: $default-border;
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
} }