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