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,
"ModeratorModule": true,
"PermissionModule": true,
"PresenceModule": true,
"ApiOnly": true
"PresenceModule": true
}
}

View File

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

View File

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

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

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

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

View File

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

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

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 bot_api.filter.discord.server_select_criteria import ServerSelectCriteria
from bot_data.filtered_result import FilteredResult
from bot_data.model.server import Server
@ -13,6 +15,9 @@ class ServerRepositoryABC(ABC):
@abstractmethod
def get_servers(self) -> List[Server]: pass
@abstractmethod
def get_filtered_servers(self, criteria: ServerSelectCriteria) -> FilteredResult: pass
@abstractmethod
def get_server_by_id(self, id: int) -> Server: pass

View File

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

View File

@ -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`)
)
""")
)

View File

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

View File

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

View File

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

View File

@ -44,6 +44,21 @@ class UserRepositoryService(UserRepositoryABC):
self._servers.get_server_by_id(result[3]),
id=result[0]
)
def find_user_by_id(self, id: int) -> Optional[User]:
self._logger.trace(__name__, f'Send SQL command: {User.get_select_by_id_string(id)}')
result = self._context.select(User.get_select_by_id_string(id))
if result is None or len(result) == 0:
return None
result = result[0]
return User(
result[1],
result[2],
self._servers.get_server_by_id(result[3]),
id=result[0]
)
def get_users_by_discord_id(self, discord_id: int) -> List[User]:
users = List(User)

4
kdb-web/.prettierrc Normal file
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 = [
{ 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) },

View File

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

View File

@ -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' },
]
},
);
}
}

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.loadNextPage();
this.loadNextPage();
}
setFilterForm() {

View File

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

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 { 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']);
}
}

View File

@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './components/dashboard/dashboard.component';
const routes: Routes = [
{path: '', component: DashboardComponent}
{ path: '', component: DashboardComponent }
];
@NgModule({

View File

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

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 { 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'
})
});
}
}

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 = isSidebarOpen;
this.sidebarWidth$.next(isSidebarOpen ? '150px' : '50px');
this.sidebarWidth$.next(isSidebarOpen ? '175px' : '75px');
});
this.sidebarWidth$.subscribe(sidebarWidth => {
this.sidebarWidth = sidebarWidth;

View File

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

View File

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

View File

@ -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 !! */
}

View File

@ -15,16 +15,9 @@
$primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor3;
$default-border: 2px solid $secondaryBackgroundColor3;
background-color: $primaryBackgroundColor !important;
html,
body {
margin: 0;
font-size: 16px;
}
background-color: $primaryBackgroundColor;
h1,
h2 {
@ -122,6 +115,35 @@
.content-divider {
border-bottom: $default-border;
}
.server-list-wrapper {
.server-filter {
}
.server-count {
}
.server-list {
.server {
border: $default-border;
border-radius: 15px;
.logo {
img {
border-radius: 100%;
}
}
.name {
color: $primaryHeaderColor;
}
&:hover {
border-color: $primaryHeaderColor !important;
}
}
}
}
}
}
}
@ -228,28 +250,43 @@
stroke: $primaryHeaderColor !important;
}
.p-menu {
.p-menu,
.p-panelmenu {
color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text,
.p-menuitem-link .p-menuitem-icon {
.p-menuitem-link .p-menuitem-icon,
.p-panelmenu-header > a {
color: $primaryTextColor !important;
background: transparent !important;
font-size: 1rem !important;
font-weight: normal !important;
}
.p-menuitem-link:focus {
.p-menuitem-link:focus,
.p-panelmenu-header > a:focus,
.p-panelmenu-content .p-menuitem .p-menuitem-link:focus {
box-shadow: none !important;
}
.p-menuitem-link:hover {
.p-menuitem-link:hover,
.p-panelmenu-header > a:hover,
.p-panelmenu-content .p-menuitem .p-menuitem-link:hover {
background-color: $secondaryBackgroundColor !important;
$border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text,
.p-menuitem-icon {
.p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important;
}
}
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
}
.p-menu-overlay {
@ -388,6 +425,9 @@
input,
.p-password {
border-radius: 10px;
border: $default-border;
&:focus {
box-shadow: none !important;
}
@ -432,7 +472,7 @@
color: $primaryTextColor !important;
border: 0 !important;
padding: 0px !important;
&:hover {
background-color: transparent !important;
color: $primaryHeaderColor !important;

View File

@ -15,14 +15,9 @@
$primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor;
$default-border: 2px solid $secondaryBackgroundColor;
html,
body {
margin: 0;
font-size: 16px;
}
background-color: $primaryBackgroundColor;
h1,
h2 {
@ -120,6 +115,35 @@
.content-divider {
border-bottom: $default-border;
}
.server-list-wrapper {
.server-filter {
}
.server-count {
}
.server-list {
.server {
border: $default-border;
border-radius: 15px;
.logo {
img {
border-radius: 100%;
}
}
.name {
color: $primaryHeaderColor;
}
&:hover {
border-color: $primaryHeaderColor !important;
}
}
}
}
}
}
}
@ -226,28 +250,43 @@
stroke: $primaryHeaderColor !important;
}
.p-menu {
.p-menu,
.p-panelmenu {
color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text,
.p-menuitem-link .p-menuitem-icon {
.p-menuitem-link .p-menuitem-icon,
.p-panelmenu-header > a {
color: $primaryTextColor !important;
background: transparent !important;
font-size: 1rem !important;
font-weight: normal !important;
}
.p-menuitem-link:focus {
.p-menuitem-link:focus,
.p-panelmenu-header > a:focus,
.p-panelmenu-content .p-menuitem .p-menuitem-link:focus {
box-shadow: none !important;
}
.p-menuitem-link:hover {
.p-menuitem-link:hover,
.p-panelmenu-header > a:hover,
.p-panelmenu-content .p-menuitem .p-menuitem-link:hover {
background-color: $secondaryBackgroundColor !important;
$border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text,
.p-menuitem-icon {
.p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important;
}
}
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
}
.p-menu-overlay {
@ -386,6 +425,9 @@
input,
.p-password {
border-radius: 10px;
border: $default-border;
&:focus {
box-shadow: none !important;
}
@ -430,7 +472,7 @@
color: $primaryTextColor !important;
border: 0 !important;
padding: 0px !important;
&:hover {
background-color: transparent !important;
color: $primaryHeaderColor !important;

View File

@ -15,16 +15,9 @@
$primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor3;
$default-border: 2px solid $secondaryBackgroundColor3;
background-color: $primaryBackgroundColor !important;
html,
body {
margin: 0;
font-size: 16px;
}
background-color: $primaryBackgroundColor;
h1,
h2 {
@ -122,6 +115,35 @@
.content-divider {
border-bottom: $default-border;
}
.server-list-wrapper {
.server-filter {
}
.server-count {
}
.server-list {
.server {
border: $default-border;
border-radius: 15px;
.logo {
img {
border-radius: 100%;
}
}
.name {
color: $primaryHeaderColor;
}
&:hover {
border-color: $primaryHeaderColor !important;
}
}
}
}
}
}
}
@ -230,28 +252,43 @@
stroke: $primaryHeaderColor !important;
}
.p-menu {
.p-menu,
.p-panelmenu {
color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text,
.p-menuitem-link .p-menuitem-icon {
.p-menuitem-link .p-menuitem-icon,
.p-panelmenu-header > a {
color: $primaryTextColor !important;
background: transparent !important;
font-size: 1rem !important;
font-weight: normal !important;
}
.p-menuitem-link:focus {
.p-menuitem-link:focus,
.p-panelmenu-header > a:focus,
.p-panelmenu-content .p-menuitem .p-menuitem-link:focus {
box-shadow: none !important;
}
.p-menuitem-link:hover {
.p-menuitem-link:hover,
.p-panelmenu-header > a:hover,
.p-panelmenu-content .p-menuitem .p-menuitem-link:hover {
background-color: $secondaryBackgroundColor !important;
$border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text,
.p-menuitem-icon {
.p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important;
}
}
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
}
.p-menu-overlay {
@ -390,6 +427,9 @@
input,
.p-password {
border-radius: 10px;
border: $default-border;
&:focus {
box-shadow: none !important;
}
@ -434,7 +474,7 @@
color: $primaryTextColor !important;
border: 0 !important;
padding: 0px !important;
&:hover {
background-color: transparent !important;
color: $primaryHeaderColor !important;

View File

@ -15,14 +15,9 @@
$primaryErrorColor: #b00020;
$secondaryErrorColor: #e94948;
$default-border: 1px solid $secondaryBackgroundColor;
$default-border: 2px solid $secondaryBackgroundColor;
html,
body {
margin: 0;
font-size: 16px;
}
background-color: $primaryBackgroundColor;
h1,
h2 {
@ -121,6 +116,35 @@
border-bottom: $default-border;
}
}
.server-list-wrapper {
.server-filter {
}
.server-count {
}
.server-list {
.server {
border: $default-border;
border-radius: 15px;
.logo {
img {
border-radius: 100%;
}
}
.name {
color: $primaryHeaderColor;
}
&:hover {
border-color: $primaryHeaderColor !important;
}
}
}
}
}
}
}
@ -226,28 +250,43 @@
stroke: $primaryHeaderColor !important;
}
.p-menu {
.p-menu,
.p-panelmenu {
color: $primaryTextColor !important;
.p-menuitem-link .p-menuitem-text,
.p-menuitem-link .p-menuitem-icon {
.p-menuitem-link .p-menuitem-icon,
.p-panelmenu-header > a {
color: $primaryTextColor !important;
background: transparent !important;
font-size: 1rem !important;
font-weight: normal !important;
}
.p-menuitem-link:focus {
.p-menuitem-link:focus,
.p-panelmenu-header > a:focus,
.p-panelmenu-content .p-menuitem .p-menuitem-link:focus {
box-shadow: none !important;
}
.p-menuitem-link:hover {
.p-menuitem-link:hover,
.p-panelmenu-header > a:hover,
.p-panelmenu-content .p-menuitem .p-menuitem-link:hover {
background-color: $secondaryBackgroundColor !important;
$border-radius: 20px;
border-radius: $border-radius 0px 0px $border-radius;
.p-menuitem-text,
.p-menuitem-icon {
.p-menuitem-icon,
.p-menuitem-text,
.p-panelmenu-icon {
color: $primaryHeaderColor !important;
}
}
.p-panelmenu-content {
margin: 5px 0px 5px 10px;
}
}
.p-menu-overlay {
@ -386,6 +425,9 @@
input,
.p-password {
border-radius: 10px;
border: $default-border;
&:focus {
box-shadow: none !important;
}
@ -430,7 +472,7 @@
color: $primaryTextColor !important;
border: 0 !important;
padding: 0px !important;
&:hover {
background-color: transparent !important;
color: $primaryHeaderColor !important;