Added flask support #70 #75 #71

Merged
edraft merged 107 commits from #70 into 0.3 2022-11-05 13:55:42 +01:00
57 changed files with 1221 additions and 134 deletions
Showing only changes of commit f553779797 - Show all commits

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)
edraft marked this conversation as resolved
Review

Was passiert, wenn eine andere E-Mail-Adresse angegeben wird?
Wenn eine positive Antwort gegeben wird: Ist es gewollt, dass ein User eine andere E-Mail-Adresse aufrufen kann?
Wenn ja: Soll ein User eine E-Mail-Adresse abfragen können, mit dem dieser nichts zu tun hat?

Was passiert, wenn eine andere E-Mail-Adresse angegeben wird? Wenn eine positive Antwort gegeben wird: Ist es gewollt, dass ein User eine andere E-Mail-Adresse aufrufen kann? Wenn ja: Soll ein User eine E-Mail-Adresse abfragen können, mit dem dieser nichts zu tun hat?
Review

was spricht dagegen?
So kannst du Profile von anderen usern sehen, kannst damit ja nichts ans pw kommen.

was spricht dagegen? So kannst du Profile von anderen usern sehen, kannst damit ja nichts ans pw kommen.
Review

Naja, so könnte sich jemand registrieren und dann somit eine Liste von gültigen E-Mails erstellen für Spammers.

Naja, so könnte sich jemand registrieren und dann somit eine Liste von gültigen E-Mails erstellen für Spammers.
Review

Wenn der User nur Profile von anderen Usern aufrufen kann, mit dem er zu tun hat, dann wäre es besser.

Wenn der User nur Profile von anderen Usern aufrufen kann, mit dem er zu tun hat, dann wäre es besser.
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'])
edraft marked this conversation as resolved Outdated

Rückgabewert von

self._icon_url

wird in der Property als

Optional[str]

angegeben und hier mit

int(values['iconURL'])

zugewiesen.

Vielleicht klassischer Copy&Paste error?

Rückgabewert von ```python self._icon_url ``` wird in der Property als ```python Optional[str] ``` angegeben und hier mit ```python int(values['iconURL']) ``` zugewiesen. Vielleicht klassischer Copy&Paste error?
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):
edraft marked this conversation as resolved
Review

Vorschlag: Die HTTP-Response-Status-Codes in einem Enum packen, damit man schneller erkennen kann, um was die Response handelt.

Vorschlag: Die HTTP-Response-Status-Codes in einem Enum packen, damit man schneller erkennen kann, um was die Response handelt.
Review

Kann man später mal nachbessern aber die API ist jetzt noch übersichtlich genug

Kann man später mal nachbessern aber die API ist jetzt noch übersichtlich genug
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,
edraft marked this conversation as resolved Outdated

Warum ist der Defaultwert für ein Integer als ein String angegeben?

Warum ist der Defaultwert für ein Integer als ein String angegeben?
`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;