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