Compare commits

..

1 Commits

Author SHA1 Message Date
f0c46a2649 Changed to asgi
Some checks failed
Test before pr merge / test-before-merge (pull_request) Has been cancelled
2025-03-08 08:20:58 +01:00
118 changed files with 1006 additions and 3054 deletions

View File

@ -1,29 +0,0 @@
name: Test before pr merge
run-name: Test before pr merge
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
jobs:
test-lint:
runs-on: [ runner ]
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
steps:
- name: Clone Repository
uses: https://github.com/actions/checkout@v3
with:
token: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Installing dependencies
working-directory: ./api
run: |
python3.12 -m pip install -r requirements-dev.txt
- name: Checking black
working-directory: ./api
run: python3.12 -m black src --check

View File

@ -0,0 +1,39 @@
name: Test before pr merge
run-name: Test before pr merge
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
jobs:
test-before-merge:
runs-on: [ runner ]
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
steps:
- name: Clone Repository
uses: https://github.com/actions/checkout@v3
with:
token: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Setup node
uses: https://github.com/actions/setup-node@v3
- name: Installing dependencies
run: npm ci
- name: Checking eslint
run: npm run lint
- name: Setup chrome
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update
apt-get install -y google-chrome-stable xvfb
- name: Testing
run: npm run test:ci

View File

@ -1,79 +0,0 @@
name: Test before pr merge
run-name: Test before pr merge
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
jobs:
test-lint:
runs-on: [ runner ]
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
steps:
- name: Clone Repository
uses: https://github.com/actions/checkout@v3
with:
token: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Setup node
uses: https://github.com/actions/setup-node@v3
- name: Installing dependencies
working-directory: ./web
run: npm ci
- name: Checking eslint
working-directory: ./web
run: npm run lint
test-translation-lint:
runs-on: [ runner ]
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
steps:
- name: Clone Repository
uses: https://github.com/actions/checkout@v3
with:
token: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Setup node
uses: https://github.com/actions/setup-node@v3
- name: Installing dependencies
working-directory: ./web
run: npm ci
- name: Checking translations
working-directory: ./web
run: npm run lint:translations
test-before-merge:
runs-on: [ runner ]
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
steps:
- name: Clone Repository
uses: https://github.com/actions/checkout@v3
with:
token: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Setup node
uses: https://github.com/actions/setup-node@v3
- name: Installing dependencies
working-directory: ./web
run: npm ci
- name: Setup chrome
working-directory: ./web
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update
apt-get install -y google-chrome-stable xvfb
- name: Testing
working-directory: ./web
run: npm run test:ci

View File

@ -9,4 +9,3 @@ starlette==0.46.0
requests==2.32.3
python-keycloak==5.3.1
python-multipart==0.0.20
websockets==15.0

View File

@ -2,12 +2,12 @@ import functools
from functools import wraps
from inspect import iscoroutinefunction
from typing import Callable, Union, Optional
from urllib.request import Request
from starlette.requests import Request
from starlette.routing import Route as StarletteRoute
from flask import request
from flask_cors import cross_origin
from api.errors import unauthorized
from api.middleware.request import get_request
from api.route_user_extension import RouteUserExtension
from core.environment import Environment
from data.schemas.administration.api_key import ApiKey
@ -16,10 +16,10 @@ from data.schemas.administration.user import User
class Route(RouteUserExtension):
registered_routes: list[StarletteRoute] = []
registered_routes = {}
@classmethod
async def get_api_key(cls, request: Request) -> ApiKey:
async def get_api_key(cls) -> ApiKey:
auth_header = request.headers.get("Authorization", None)
api_key = auth_header.split(" ")[1]
return await apiKeyDao.find_by_key(api_key)
@ -35,55 +35,41 @@ class Route(RouteUserExtension):
return api_key_from_db is not None and not api_key_from_db.deleted
@classmethod
async def _get_auth_type(
cls, request: Request, auth_header: str
) -> Optional[Union[User, ApiKey]]:
async def _get_auth_type(cls, auth_header: str) -> Optional[Union[User, ApiKey]]:
if auth_header.startswith("Bearer "):
return await cls.get_user()
elif auth_header.startswith("API-Key "):
return await cls.get_api_key(request)
return await cls.get_api_key()
elif (
auth_header.startswith("DEV-User ")
and Environment.get_environment() == "development"
auth_header.startswith("DEV-User ")
and Environment.get_environment() == "development"
):
return await cls.get_dev_user()
return None
@classmethod
async def get_authenticated_user_or_api_key(cls) -> Union[User, ApiKey]:
request = get_request()
if request is None:
raise ValueError("No request found")
auth_header = request.headers.get("Authorization", None)
if not auth_header:
raise Exception("No Authorization header found")
user_or_api_key = await cls._get_auth_type(request, auth_header)
user_or_api_key = await cls._get_auth_type(auth_header)
if user_or_api_key is None:
raise Exception("Invalid Authorization header")
return user_or_api_key
@classmethod
async def get_authenticated_user_or_api_key_or_default(
cls,
cls,
) -> Optional[Union[User, ApiKey]]:
request = get_request()
if request is None:
return None
auth_header = request.headers.get("Authorization", None)
if not auth_header:
return None
return await cls._get_auth_type(request, auth_header)
return await cls._get_auth_type(auth_header)
@classmethod
async def is_authorized(cls) -> bool:
request = get_request()
if request is None:
return False
auth_header = request.headers.get("Authorization", None)
if not auth_header:
return False
@ -93,8 +79,8 @@ class Route(RouteUserExtension):
elif auth_header.startswith("API-Key "):
return await cls._verify_api_key(request)
elif (
auth_header.startswith("DEV-User ")
and Environment.get_environment() == "development"
auth_header.startswith("DEV-User ")
and Environment.get_environment() == "development"
):
user = await cls.get_dev_user()
return user is not None
@ -102,10 +88,10 @@ class Route(RouteUserExtension):
@classmethod
def authorize(
cls,
f: Callable = None,
skip_in_dev=False,
by_api_key=False,
cls,
f: Callable = None,
skip_in_dev=False,
by_api_key=False,
):
if f is None:
return functools.partial(
@ -113,25 +99,26 @@ class Route(RouteUserExtension):
)
@wraps(f)
async def decorator(request: Request, *args, **kwargs):
async def decorator(*args, **kwargs):
if skip_in_dev and Environment.get_environment() == "development":
if iscoroutinefunction(f):
return await f(request, *args, **kwargs)
return f(request, *args, **kwargs)
return await f(*args, **kwargs)
return f(*args, **kwargs)
if not await cls.is_authorized():
return unauthorized()
if iscoroutinefunction(f):
return await f(request, *args, **kwargs)
return f(request, *args, **kwargs)
return await f(*args, **kwargs)
return f(*args, **kwargs)
return decorator
@classmethod
def route(cls, path=None, **kwargs):
def inner(fn):
cls.registered_routes.append(StarletteRoute(path, fn, **kwargs))
cross_origin(fn)
cls.registered_routes[path] = (fn, kwargs)
return fn
return inner

View File

@ -1,11 +1,10 @@
from typing import Optional
from flask import request, Request, has_request_context
from keycloak import KeycloakAuthenticationError, KeycloakConnectionError
from starlette.requests import Request
from api.auth.keycloak_client import Keycloak
from api.auth.keycloak_user import KeycloakUser
from api.middleware.request import get_request
from core.get_value import get_value
from core.logger import Logger
from data.schemas.administration.user import User
@ -20,8 +19,8 @@ logger = Logger(__name__)
class RouteUserExtension:
@classmethod
def _get_user_id_from_token(cls, request: Request) -> Optional[str]:
token = cls.get_token(request)
def _get_user_id_from_token(cls) -> Optional[str]:
token = cls.get_token()
if not token:
return None
@ -35,7 +34,7 @@ class RouteUserExtension:
return get_value(user_info, "sub", str)
@staticmethod
def get_token(request: Request) -> Optional[str]:
def get_token() -> Optional[str]:
if "Authorization" not in request.headers:
return None
@ -46,24 +45,23 @@ class RouteUserExtension:
@classmethod
async def get_user(cls) -> Optional[User]:
request = get_request()
if request is None:
if not has_request_context():
return None
user_id = cls._get_user_id_from_token(request)
user_id = cls._get_user_id_from_token()
if not user_id:
return None
return await userDao.find_by_keycloak_id(user_id)
user = await userDao.find_by_keycloak_id(user_id)
if user is None:
return None
return user
@classmethod
async def get_dev_user(cls) -> Optional[User]:
request = get_request()
if request is None:
return None
return await userDao.find_single_by(
[{User.keycloak_id: cls.get_token(request)}, {User.deleted: False}]
[{User.keycloak_id: cls.get_token()}, {User.deleted: False}]
)
@classmethod
@ -73,6 +71,56 @@ class RouteUserExtension:
user = await cls.get_dev_user()
return user
@classmethod
def _flatten_groups(cls, groups):
flat_list = []
for group in groups:
flat_list.append(group)
if "subGroups" in group and group["subGroups"]:
flat_list.extend(cls._flatten_groups(group["subGroups"]))
return flat_list
@classmethod
async def _map_keycloak_groups_with_roles(cls, user: User):
try:
roles = {x.name: x for x in await roleDao.get_all()}
groups = cls._flatten_groups(Keycloak.admin.get_groups(full_hierarchy=True))
groups_with_role = [x["name"] for x in groups if x["name"] in roles.keys()]
user_groups_with_role = [
x["name"]
for x in Keycloak.admin.get_user_groups(user.keycloak_id)
if x["name"] in roles.keys()
]
user_roles = set(
x.name for x in await user.roles if x.name in groups_with_role
)
missing_groups = set(user_groups_with_role) - set(user_roles)
missing_roles = set(user_roles) - set(user_groups_with_role)
if len(missing_groups) > 0:
await roleUserDao.create_many(
[
RoleUser(0, (await roleDao.get_by_name(group)).id, user.id)
for group in missing_groups
]
)
if len(missing_roles) > 0:
await roleUserDao.delete_many(
[
await roleUserDao.get_single_by(
[
{RoleUser.role_id: roles[role].id},
{RoleUser.user_id: user.id},
]
)
for role in missing_roles
]
)
except Exception as e:
logger.error("Failed to map user groups", e)
@classmethod
async def _create_user(cls, kc_user: KeycloakUser):
try:
@ -92,8 +140,8 @@ class RouteUserExtension:
logger.error("Failed to find or create user", e)
@classmethod
async def verify_login(cls, request: Request) -> bool:
auth_header = request.headers.get("Authorization", None)
async def verify_login(cls, req: Request) -> bool:
auth_header = req.headers.get("Authorization", None)
if not auth_header or not auth_header.startswith("Bearer "):
return False
@ -107,8 +155,11 @@ class RouteUserExtension:
user = await cls.get_user()
if user is None:
await cls._create_user(KeycloakUser(user_info))
u_id = await cls._create_user(KeycloakUser(user_info))
await cls._map_keycloak_groups_with_roles(await userDao.get_by_id(u_id))
return True
else:
await cls._map_keycloak_groups_with_roles(user)
if user.deleted:
return False

View File

@ -4,7 +4,6 @@ from api_graphql.abc.filter.bool_filter import BoolFilter
from api_graphql.abc.filter.int_filter import IntFilter
from api_graphql.abc.filter.string_filter import StringFilter
from api_graphql.abc.filter_abc import FilterABC
from api_graphql.filter.fuzzy_filter import FuzzyFilter
class DbModelFilterABC[T](FilterABC[T]):
@ -19,5 +18,3 @@ class DbModelFilterABC[T](FilterABC[T]):
self.add_field("editor", IntFilter)
self.add_field("createdUtc", StringFilter, "created")
self.add_field("updatedUtc", StringFilter, "updated")
self.add_field("fuzzy", FuzzyFilter)

View File

@ -4,13 +4,12 @@ from enum import Enum
from types import NoneType
from typing import Callable, Type, get_args, Any, Union
from ariadne import ObjectType, SubscriptionType
from ariadne import ObjectType
from graphql import GraphQLResolveInfo
from typing_extensions import deprecated
from api.route import Route
from api_graphql.abc.collection_filter_abc import CollectionFilterABC
from api_graphql.abc.field_abc import FieldABC
from api_graphql.abc.input_abc import InputABC
from api_graphql.abc.sort_abc import Sort
from api_graphql.field.collection_field import CollectionField
@ -21,7 +20,6 @@ from api_graphql.field.mutation_field import MutationField
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
from api_graphql.field.resolver_field import ResolverField
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
from api_graphql.field.subscription_field import SubscriptionField
from api_graphql.service.collection_result import CollectionResult
from api_graphql.service.exceptions import (
UnauthorizedException,
@ -31,7 +29,6 @@ from api_graphql.service.exceptions import (
from api_graphql.service.query_context import QueryContext
from api_graphql.typing import TRequireAnyPermissions, TRequireAnyResolvers
from core.logger import APILogger
from core.string import first_to_lower
from service.permission.permissions_enum import Permissions
logger = APILogger(__name__)
@ -43,7 +40,6 @@ class QueryABC(ObjectType):
@abstractmethod
def __init__(self, name: str = __name__):
ObjectType.__init__(self, name)
self._subscriptions: dict[str, SubscriptionType] = {}
@staticmethod
async def _authorize():
@ -64,19 +60,19 @@ class QueryABC(ObjectType):
@classmethod
async def _require_any(
cls,
data: Any,
permissions: TRequireAnyPermissions,
resolvers: TRequireAnyResolvers,
*args,
**kwargs,
cls,
data: Any,
permissions: TRequireAnyPermissions,
resolvers: TRequireAnyResolvers,
*args,
**kwargs,
):
info = args[0]
if len(permissions) > 0:
user = await Route.get_authenticated_user_or_api_key_or_default()
perms = await user.permissions
has_perms = [await user.has_permission(x) for x in permissions]
if user is not None and all(
[await user.has_permission(x) for x in permissions]
has_perms
):
return
@ -97,13 +93,13 @@ class QueryABC(ObjectType):
raise AccessDenied()
def field(
self,
builder: Union[
DaoFieldBuilder,
CollectionFieldBuilder,
ResolverFieldBuilder,
MutationFieldBuilder,
],
self,
builder: Union[
DaoFieldBuilder,
CollectionFieldBuilder,
ResolverFieldBuilder,
MutationFieldBuilder,
],
):
"""
Add a field to the query
@ -138,12 +134,7 @@ class QueryABC(ObjectType):
skip = kwargs["skip"]
collection = await field.dao.find_by(filters, sorts, take, skip)
if field.direct_result:
return collection
res = CollectionResult(
await field.dao.count(filters), len(collection), collection
)
res = CollectionResult(await field.dao.count(), len(collection), collection)
return res
async def collection_wrapper(*args, **kwargs):
@ -180,12 +171,11 @@ class QueryABC(ObjectType):
)
async def resolver_wrapper(*args, **kwargs):
result = (
return (
await field.resolver(*args, **kwargs)
if iscoroutinefunction(field.resolver)
else field.resolver(*args, **kwargs)
)
return result
if isinstance(field, DaoField):
resolver = dao_wrapper
@ -199,7 +189,7 @@ class QueryABC(ObjectType):
elif isinstance(field, MutationField):
async def input_wrapper(
mutation: QueryABC, info: GraphQLResolveInfo, **kwargs
mutation: QueryABC, info: GraphQLResolveInfo, **kwargs
):
if field.input_type is None:
return await resolver_wrapper(mutation, info, **kwargs)
@ -215,13 +205,6 @@ class QueryABC(ObjectType):
resolver = input_wrapper
elif isinstance(field, SubscriptionField):
async def sub_wrapper(sub: QueryABC, info: GraphQLResolveInfo, **kwargs):
return await resolver_wrapper(sub, info, **kwargs)
resolver = sub_wrapper
else:
raise ValueError(f"Unknown field type: {field.name}")
@ -230,21 +213,16 @@ class QueryABC(ObjectType):
await self._authorize()
if (
field.require_any is None
and not field.public
and field.require_any_permission
field.require_any is None
and not field.public
and field.require_any_permission
):
await self._require_any_permission(field.require_any_permission)
result = await resolver(*args, **kwargs)
if field.require_any is not None:
await self._require_any(
result,
*field.require_any,
*args,
**kwargs,
)
await self._require_any(result, *field.require_any, *args, **kwargs)
return result
@ -252,13 +230,13 @@ class QueryABC(ObjectType):
@deprecated("Use field(FieldBuilder()) instead")
def mutation(
self,
name: str,
f: Callable,
input_type: Type[InputABC] = None,
input_key: str = "input",
require_any_permission: list[Permissions] = None,
public: bool = False,
self,
name: str,
f: Callable,
input_type: Type[InputABC] = None,
input_key: str = "input",
require_any_permission: list[Permissions] = None,
public: bool = False,
):
"""
Adds a mutation to the query
@ -274,9 +252,6 @@ class QueryABC(ObjectType):
self.field(
MutationFieldBuilder(name)
.with_resolver(f)
.with_change_broadcast(
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
)
.with_input(input_type, input_key)
.with_require_any_permission(require_any_permission)
.with_public(public)
@ -284,13 +259,13 @@ class QueryABC(ObjectType):
@classmethod
def _resolve_collection(
cls,
collection: list,
*_,
filters: list[CollectionFilterABC] = None,
sort: list[Sort] = None,
skip: int = None,
take: int = None,
cls,
collection: list,
*_,
filters: list[CollectionFilterABC] = None,
sort: list[Sort] = None,
skip: int = None,
take: int = None,
) -> CollectionResult:
total_count = len(collection)
@ -298,8 +273,6 @@ class QueryABC(ObjectType):
for f in filters:
collection = list(filter(lambda x: f.filter(x), collection))
total_count = len(collection)
if sort is not None:
def f_sort(x: object, k: str):
@ -313,7 +286,7 @@ class QueryABC(ObjectType):
return attr
for s in reversed(
sort
sort
): # Apply sorting in reverse order to make first primary "orderBy" and other secondary "thenBy"
attrs = [a for a in dir(s) if not a.startswith("_")]
for k in attrs:

View File

@ -1,50 +0,0 @@
from abc import abstractmethod
from asyncio import iscoroutinefunction
from ariadne import SubscriptionType
from api_graphql.abc.query_abc import QueryABC
from api_graphql.field.subscription_field_builder import SubscriptionFieldBuilder
from core.logger import APILogger
logger = APILogger(__name__)
class SubscriptionABC(SubscriptionType, QueryABC):
@abstractmethod
def __init__(self):
SubscriptionType.__init__(self)
def subscribe(self, builder: SubscriptionFieldBuilder):
field = builder.build()
async def wrapper(*args, **kwargs):
if not field.public:
await self._authorize()
if (
field.require_any is None
and not field.public
and field.require_any_permission
):
await self._require_any_permission(field.require_any_permission)
result = (
await field.resolver(*args, **kwargs)
if iscoroutinefunction(field.resolver)
else field.resolver(*args, **kwargs)
)
if field.require_any is not None:
await self._require_any(
result,
*field.require_any,
*args,
**kwargs,
)
return result
self.set_field(field.name, wrapper)
self.set_source(field.name, field.generator)

View File

@ -4,7 +4,6 @@ import os
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.abc.query_abc import QueryABC
from api_graphql.abc.subscription_abc import SubscriptionABC
from api_graphql.query import Query
@ -20,7 +19,7 @@ def import_graphql_schema_part(part: str):
import_graphql_schema_part("queries")
import_graphql_schema_part("mutations")
sub_query_classes = [DbModelQueryABC, MutationABC, SubscriptionABC]
sub_query_classes = [DbModelQueryABC, MutationABC]
query_classes = [
*[y for x in sub_query_classes for y in x.__subclasses__()],
*[x for x in QueryABC.__subclasses__() if x not in sub_query_classes],

View File

@ -20,7 +20,6 @@ class DaoField(FieldABC):
dao: DataAccessObjectABC = None,
filter_type: Type[FilterABC] = None,
sort_type: Type[T] = None,
direct_result: bool = False,
):
FieldABC.__init__(self, name, require_any_permission, require_any, public)
self._name = name
@ -29,7 +28,6 @@ class DaoField(FieldABC):
self._dao = dao
self._filter_type = filter_type
self._sort_type = sort_type
self._direct_result = direct_result
@property
def dao(self) -> Optional[DataAccessObjectABC]:
@ -44,7 +42,3 @@ class DaoField(FieldABC):
@property
def sort_type(self) -> Optional[Type[T]]:
return self._sort_type
@property
def direct_result(self) -> bool:
return self._direct_result

View File

@ -15,7 +15,6 @@ class DaoFieldBuilder(FieldBuilderABC):
self._dao = None
self._filter_type = None
self._sort_type = None
self._direct_result = False
def with_dao(self, dao: DataAccessObjectABC) -> Self:
assert dao is not None, "dao cannot be None"
@ -32,10 +31,6 @@ class DaoFieldBuilder(FieldBuilderABC):
self._sort_type = sort_type
return self
def with_direct_result(self) -> Self:
self._direct_result = True
return self
def build(self) -> DaoField:
assert self._dao is not None, "dao cannot be None"
return DaoField(
@ -46,5 +41,4 @@ class DaoFieldBuilder(FieldBuilderABC):
self._dao,
self._filter_type,
self._sort_type,
self._direct_result,
)

View File

@ -1,9 +1,7 @@
from asyncio import iscoroutinefunction
from typing import Self, Type
from ariadne.types import Resolver
from api.broadcast import broadcast
from api_graphql.abc.field_builder_abc import FieldBuilderABC
from api_graphql.abc.input_abc import InputABC
from api_graphql.field.mutation_field import MutationField
@ -20,41 +18,9 @@ class MutationFieldBuilder(FieldBuilderABC):
def with_resolver(self, resolver: Resolver) -> Self:
assert resolver is not None, "resolver cannot be None"
self._resolver = resolver
return self
def with_broadcast(self, source: str):
assert self._resolver is not None, "resolver cannot be None for broadcast"
resolver = self._resolver
async def resolver_wrapper(*args, **kwargs):
result = (
await resolver(*args, **kwargs)
if iscoroutinefunction(resolver)
else resolver(*args, **kwargs)
)
await broadcast.publish(f"{source}", result)
return result
def with_change_broadcast(self, source: str):
assert self._resolver is not None, "resolver cannot be None for broadcast"
resolver = self._resolver
async def resolver_wrapper(*args, **kwargs):
result = (
await resolver(*args, **kwargs)
if iscoroutinefunction(resolver)
else resolver(*args, **kwargs)
)
await broadcast.publish(f"{source}", {})
return result
self._resolver = resolver_wrapper
return self
def with_input(self, input_type: Type[InputABC], input_key: str = None) -> Self:
self._input_type = input_type
self._input_key = input_key

View File

@ -16,17 +16,11 @@ class ResolverField(FieldABC):
require_any: TRequireAny = None,
public: bool = False,
resolver: Resolver = None,
direct_result: bool = False,
):
FieldABC.__init__(self, name, require_any_permission, require_any, public)
self._resolver = resolver
self._direct_result = direct_result
@property
def resolver(self) -> Optional[Resolver]:
return self._resolver
@property
def direct_result(self) -> bool:
return self._direct_result

View File

@ -12,17 +12,12 @@ class ResolverFieldBuilder(FieldBuilderABC):
FieldBuilderABC.__init__(self, name)
self._resolver = None
self._direct_result = False
def with_resolver(self, resolver: Resolver) -> Self:
assert resolver is not None, "resolver cannot be None"
self._resolver = resolver
return self
def with_direct_result(self) -> Self:
self._direct_result = True
return self
def build(self) -> ResolverField:
assert self._resolver is not None, "resolver cannot be None"
return ResolverField(
@ -31,5 +26,4 @@ class ResolverFieldBuilder(FieldBuilderABC):
self._require_any,
self._public,
self._resolver,
self._direct_result,
)

View File

@ -1,32 +0,0 @@
from typing import Optional
from ariadne.types import Resolver
from api_graphql.abc.field_abc import FieldABC
from api_graphql.typing import TRequireAny
from service.permission.permissions_enum import Permissions
class SubscriptionField(FieldABC):
def __init__(
self,
name: str,
require_any_permission: list[Permissions] = None,
require_any: TRequireAny = None,
public: bool = False,
resolver: Resolver = None,
generator: Resolver = None,
):
FieldABC.__init__(self, name, require_any_permission, require_any, public)
self._resolver = resolver
self._generator = generator
@property
def resolver(self) -> Optional[Resolver]:
return self._resolver
@property
def generator(self) -> Optional[Resolver]:
return self._generator

View File

@ -1,46 +0,0 @@
from typing import Self, AsyncGenerator
from ariadne.types import Resolver
from api.broadcast import broadcast
from api_graphql.abc.field_builder_abc import FieldBuilderABC
from api_graphql.field.subscription_field import SubscriptionField
class SubscriptionFieldBuilder(FieldBuilderABC):
def __init__(self, name: str):
FieldBuilderABC.__init__(self, name)
self._resolver = None
self._generator = None
def with_resolver(self, resolver: Resolver) -> Self:
assert resolver is not None, "resolver cannot be None"
self._resolver = resolver
return self
def with_generator(self, generator: Resolver) -> Self:
assert generator is not None, "generator cannot be None"
self._generator = generator
return self
def build(self) -> SubscriptionField:
assert self._resolver is not None, "resolver cannot be None"
if self._generator is None:
async def generator(*args, **kwargs) -> AsyncGenerator[str, None]:
async with broadcast.subscribe(channel=self._name) as subscriber:
async for message in subscriber:
yield message
self._generator = generator
return SubscriptionField(
self._name,
self._require_any_permission,
self._require_any,
self._public,
self._resolver,
self._generator,
)

View File

@ -1,15 +0,0 @@
from typing import Optional
from api_graphql.abc.filter_abc import FilterABC
class FuzzyFilter(FilterABC):
def __init__(
self,
obj: Optional[dict],
):
FilterABC.__init__(self, obj)
self.add_field("fields", list)
self.add_field("term", str)
self.add_field("threshold", int)

View File

@ -9,6 +9,6 @@ class ShortUrlFilter(DbModelFilterABC):
):
DbModelFilterABC.__init__(self, obj)
self.add_field("shortUrl", StringFilter, db_name="short_url")
self.add_field("targetUrl", StringFilter, db_name="target_url")
self.add_field("short_url", StringFilter)
self.add_field("target_url", StringFilter)
self.add_field("description", StringFilter)

View File

@ -21,21 +21,11 @@ input ApiKeySort {
identifier: SortOrder
deleted: SortOrder
editor: UserSort
editorId: SortOrder
createdUtc: SortOrder
updatedUtc: SortOrder
}
enum ApiKeyFuzzyFields {
identifier
}
input ApiKeyFuzzy {
fields: [ApiKeyFuzzyFields]
term: String
threshold: Int
}
input ApiKeyFilter {
id: IntFilter
identifier: StringFilter

View File

@ -26,22 +26,10 @@ input DomainSort {
updatedUtc: SortOrder
}
enum DomainFuzzyFields {
name
}
input DomainFuzzy {
fields: [DomainFuzzyFields]
term: String
threshold: Int
}
input DomainFilter {
id: IntFilter
name: StringFilter
fuzzy: DomainFuzzy
deleted: BooleanFilter
editor: IntFilter
createdUtc: DateFilter

View File

@ -1,19 +0,0 @@
type FeatureFlag implements DbModel {
id: ID
key: String
value: Boolean
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
}
type FeatureFlagMutation {
change(input: FeatureFlagInput!): FeatureFlag
}
input FeatureFlagInput {
key: String!
value: Boolean!
}

View File

@ -27,22 +27,10 @@ input GroupSort {
updatedUtc: SortOrder
}
enum GroupFuzzyFields {
name
}
input GroupFuzzy {
fields: [GroupFuzzyFields]
term: String
threshold: Int
}
input GroupFilter {
id: IntFilter
name: StringFilter
fuzzy: GroupFuzzy
deleted: BooleanFilter
editor: IntFilter
createdUtc: DateFilter

View File

@ -14,8 +14,4 @@ type Query {
domains(filter: [DomainFilter], sort: [DomainSort], skip: Int, take: Int): DomainResult
groups(filter: [GroupFilter], sort: [GroupSort], skip: Int, take: Int): GroupResult
shortUrls(filter: [ShortUrlFilter], sort: [ShortUrlSort], skip: Int, take: Int): ShortUrlResult
settings(key: String): [Setting]
userSettings(key: String): [Setting]
featureFlags(key: String): [FeatureFlag]
}

View File

@ -23,31 +23,18 @@ input RoleSort {
description: SortOrder
deleted: SortOrder
editor: UserSort
editorId: SortOrder
createdUtc: SortOrder
updatedUtc: SortOrder
}
enum RoleFuzzyFields {
name
description
}
input RoleFuzzy {
fields: [RoleFuzzyFields]
term: String
threshold: Int
}
input RoleFilter {
id: IntFilter
name: StringFilter
description: StringFilter
fuzzy: RoleFuzzy
deleted: BooleanFilter
editor_id: IntFilter
editorId: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
}

View File

@ -1,19 +0,0 @@
type Setting implements DbModel {
id: ID
key: String
value: String
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
}
type SettingMutation {
change(input: SettingInput!): Setting
}
input SettingInput {
key: String!
value: String!
}

View File

@ -32,26 +32,12 @@ input ShortUrlSort {
updatedUtc: SortOrder
}
enum ShortUrlFuzzyFields {
shortUrl
targetUrl
description
}
input ShortUrlFuzzy {
fields: [ShortUrlFuzzyFields]
term: String
threshold: Int
}
input ShortUrlFilter {
id: IntFilter
name: StringFilter
description: StringFilter
loadingScreen: BooleanFilter
fuzzy: ShortUrlFuzzy
deleted: BooleanFilter
editor: IntFilter
createdUtc: DateFilter

View File

@ -1,16 +0,0 @@
scalar SubscriptionChange
type Subscription {
ping: String
apiKeyChange: SubscriptionChange
featureFlagChange: SubscriptionChange
roleChange: SubscriptionChange
settingChange: SubscriptionChange
userChange: SubscriptionChange
userSettingChange: SubscriptionChange
domainChange: SubscriptionChange
groupChange: SubscriptionChange
shortUrlChange: SubscriptionChange
}

View File

@ -35,33 +35,19 @@ input UserSort {
email: SortOrder
deleted: SortOrder
editor: UserSort
editorId: SortOrder
createdUtc: SortOrder
updatedUtc: SortOrder
}
enum UserFuzzyFields {
keycloakId
username
email
}
input UserFuzzy {
fields: [UserFuzzyFields]
term: String
threshold: Int
}
input UserFilter {
id: IntFilter
keycloakId: StringFilter
username: StringFilter
email: StringFilter
fuzzy: UserFuzzy
deleted: BooleanFilter
editor: UserFilter
editor: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
}

View File

@ -1,19 +0,0 @@
type UserSetting implements DbModel {
id: ID
key: String
value: String
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
}
type UserSettingMutation {
change(input: UserSettingInput!): UserSetting
}
input UserSettingInput {
key: String!
value: String!
}

View File

@ -26,10 +26,6 @@ from data.schemas.public.group import Group
from data.schemas.public.group_dao import groupDao
from data.schemas.public.short_url import ShortUrl
from data.schemas.public.short_url_dao import shortUrlDao
from data.schemas.public.user_setting import UserSetting
from data.schemas.public.user_setting_dao import userSettingsDao
from data.schemas.system.feature_flag_dao import featureFlagDao
from data.schemas.system.setting_dao import settingsDao
from service.permission.permissions_enum import Permissions
@ -131,23 +127,6 @@ class Query(QueryABC):
.with_require_any([Permissions.short_urls], [group_by_assignment_resolver])
)
self.field(
ResolverFieldBuilder("settings")
.with_resolver(self._resolve_settings)
.with_direct_result()
.with_public(True)
)
self.field(
ResolverFieldBuilder("userSettings")
.with_resolver(self._resolve_user_settings)
.with_direct_result()
)
self.field(
ResolverFieldBuilder("featureFlags")
.with_resolver(self._resolve_feature_flags)
.with_direct_result()
)
@staticmethod
async def _get_user(*_):
return await Route.get_user()
@ -178,27 +157,3 @@ class Query(QueryABC):
for x in kc_users
if x["id"] not in existing_user_keycloak_ids
]
@staticmethod
async def _resolve_settings(*args, **kwargs):
if "key" in kwargs:
return [await settingsDao.find_by_key(kwargs["key"])]
return await settingsDao.get_all()
@staticmethod
async def _resolve_user_settings(*args, **kwargs):
user = await Route.get_user()
if user is None:
return None
if "key" in kwargs:
return await userSettingsDao.find_by(
{UserSetting.user_id: user.id, UserSetting.key: kwargs["key"]}
)
return await userSettingsDao.find_by({UserSetting.user_id: user.id})
@staticmethod
async def _resolve_feature_flags(*args, **kwargs):
if "key" in kwargs:
return [await featureFlagDao.find_by_key(kwargs["key"])]
return await featureFlagDao.get_all()

View File

@ -5,7 +5,6 @@ from ariadne import make_executable_schema, load_schema_from_path
from api_graphql.definition import QUERIES
from api_graphql.mutation import Mutation
from api_graphql.query import Query
from api_graphql.subscription import Subscription
type_defs = load_schema_from_path(
os.path.join(os.path.dirname(os.path.realpath(__file__)), "../graphql/")
@ -14,6 +13,5 @@ schema = make_executable_schema(
type_defs,
Query(),
Mutation(),
Subscription(),
*QUERIES,
)

View File

@ -1,66 +0,0 @@
from api_graphql.abc.subscription_abc import SubscriptionABC
from api_graphql.field.subscription_field_builder import SubscriptionFieldBuilder
from service.permission.permissions_enum import Permissions
class Subscription(SubscriptionABC):
def __init__(self):
SubscriptionABC.__init__(self)
self.subscribe(
SubscriptionFieldBuilder("ping")
.with_resolver(lambda message, *_: message.message)
.with_public(True)
)
self.subscribe(
SubscriptionFieldBuilder("apiKeyChange")
.with_resolver(lambda message, *_: message.message)
.with_require_any_permission([Permissions.api_keys])
)
self.subscribe(
SubscriptionFieldBuilder("featureFlagChange")
.with_resolver(lambda message, *_: message.message)
.with_public(True)
)
self.subscribe(
SubscriptionFieldBuilder("roleChange")
.with_resolver(lambda message, *_: message.message)
.with_require_any_permission([Permissions.roles])
)
self.subscribe(
SubscriptionFieldBuilder("settingChange")
.with_resolver(lambda message, *_: message.message)
.with_require_any_permission([Permissions.settings])
)
self.subscribe(
SubscriptionFieldBuilder("userChange")
.with_resolver(lambda message, *_: message.message)
.with_require_any_permission([Permissions.users])
)
self.subscribe(
SubscriptionFieldBuilder("userSettingChange")
.with_resolver(lambda message, *_: message.message)
.with_public(True)
)
self.subscribe(
SubscriptionFieldBuilder("domainChange")
.with_resolver(lambda message, *_: message.message)
.with_require_any_permission([Permissions.domains])
)
self.subscribe(
SubscriptionFieldBuilder("groupChange")
.with_resolver(lambda message, *_: message.message)
.with_require_any_permission([Permissions.groups])
)
self.subscribe(
SubscriptionFieldBuilder("shortUrlChange")
.with_resolver(lambda message, *_: message.message)
.with_require_any_permission([Permissions.short_urls])
)

View File

@ -1 +0,0 @@
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f %z"

View File

@ -4,12 +4,9 @@ from enum import Enum
from types import NoneType
from typing import Generic, Optional, Union, TypeVar, Any, Type
from core.const import DATETIME_FORMAT
from core.database.abc.db_model_abc import DbModelABC
from core.database.database import Database
from core.get_value import get_value
from core.logger import DBLogger
from core.string import camel_to_snake
from core.typing import T, Attribute, AttributeFilters, AttributeSorts
T_DBM = TypeVar("T_DBM", bound=DbModelABC)
@ -26,11 +23,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
self._default_filter_condition = None
self.__attributes: dict[str, type] = {}
self.__joins: dict[str, str] = {}
self.__db_names: dict[str, str] = {}
self.__foreign_tables: dict[str, str] = {}
self.__date_attributes: set[str] = set()
self.__ignored_attributes: set[str] = set()
@ -76,40 +69,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if attr_type in [datetime, datetime.datetime]:
self.__date_attributes.add(db_name)
def reference(
self,
attr: Attribute,
primary_attr: Attribute,
foreign_attr: Attribute,
table_name: str,
):
"""
Add a reference to another table for the given attribute
:param str primary_attr: Name of the primary key in the foreign object
:param str foreign_attr: Name of the foreign key in the object
:param str table_name: Name of the table to reference
:return:
"""
if table_name == self._table_name:
return
if isinstance(attr, property):
attr = attr.fget.__name__
if isinstance(primary_attr, property):
primary_attr = primary_attr.fget.__name__
primary_attr = primary_attr.lower().replace("_", "")
if isinstance(foreign_attr, property):
foreign_attr = foreign_attr.fget.__name__
foreign_attr = foreign_attr.lower().replace("_", "")
self.__joins[foreign_attr] = (
f"LEFT JOIN {table_name} ON {table_name}.{primary_attr} = {self._table_name}.{foreign_attr}"
)
self.__foreign_tables[attr] = table_name
def to_object(self, result: dict) -> T_DBM:
"""
Convert a result from the database to an object
@ -130,13 +89,8 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return self._model_type(**value_map)
async def count(self, filters: AttributeFilters = None) -> int:
query = f"SELECT COUNT(*) FROM {self._table_name}"
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
query += f" WHERE {self._build_conditions(filters)}"
result = await self._db.select_map(query)
async def count(self) -> int:
result = await self._db.select_map(f"SELECT COUNT(*) FROM {self._table_name}")
return result[0]["count"]
async def get_all(self) -> list[T_DBM]:
@ -430,12 +384,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return "ARRAY[]::text[]"
return f"ARRAY[{", ".join([DataAccessObjectABC._get_value_sql(x) for x in value])}]"
if isinstance(value, datetime.datetime):
if value.tzinfo is None:
value = value.replace(tzinfo=datetime.timezone.utc)
return f"'{value.strftime(DATETIME_FORMAT)}'"
return str(value)
@staticmethod
@ -464,10 +412,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
take: int = None,
skip: int = None,
) -> str:
query = f"SELECT {self._table_name}.* FROM {self._table_name}"
for join in self.__joins:
query += f" {self.__joins[join]}"
query = f"SELECT * FROM {self._table_name}"
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
query += f" WHERE {self._build_conditions(filters)}"
@ -493,37 +438,12 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
for attr, values in f.items():
if isinstance(attr, property):
attr = attr.fget.__name__
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
conditions.extend(
self._build_foreign_conditions(foreign_table, values)
)
continue
if attr == "fuzzy":
conditions.append(
" OR ".join(
self._build_fuzzy_conditions(
[
self.__db_names[x] if x in self.__db_names else self.__db_names[camel_to_snake(x)]
for x in get_value(values, "fields", list[str])
],
get_value(values, "term", str),
get_value(values, "threshold", int, 5),
)
)
)
continue
db_name = self.__db_names[attr]
if isinstance(values, dict):
for operator, value in values.items():
conditions.append(
self._build_condition(
f"{self._table_name}.{db_name}", operator, value
)
self._build_condition(db_name, operator, value)
)
elif isinstance(values, list):
sub_conditions = []
@ -531,9 +451,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if isinstance(value, dict):
for operator, val in value.items():
sub_conditions.append(
self._build_condition(
f"{self._table_name}.{db_name}", operator, val
)
self._build_condition(db_name, operator, val)
)
else:
sub_conditions.append(
@ -545,65 +463,12 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return " AND ".join(conditions)
def _build_fuzzy_conditions(
self, fields: list[str], term: str, threshold: int = 10
) -> list[str]:
conditions = []
for field in fields:
conditions.append(
f"levenshtein({field}, '{term}') <= {threshold}"
) # Adjust the threshold as needed
return conditions
def _build_foreign_conditions(self, table: str, values: dict) -> list[str]:
"""
Build SQL conditions for foreign key references
:param table: Foreign table name
:param values: Filter values
:return: List of conditions
"""
conditions = []
for attr, sub_values in values.items():
if isinstance(attr, property):
attr = attr.fget.__name__
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
conditions.extend(
self._build_foreign_conditions(foreign_table, sub_values)
)
continue
db_name = f"{table}.{attr.lower().replace('_', '')}"
if isinstance(sub_values, dict):
for operator, value in sub_values.items():
conditions.append(self._build_condition(db_name, operator, value))
elif isinstance(sub_values, list):
sub_conditions = []
for value in sub_values:
if isinstance(value, dict):
for operator, val in value.items():
sub_conditions.append(
self._build_condition(db_name, operator, val)
)
else:
sub_conditions.append(
self._get_value_validation_sql(db_name, value)
)
conditions.append(f"({' OR '.join(sub_conditions)})")
else:
conditions.append(self._get_value_validation_sql(db_name, sub_values))
return conditions
def _get_value_validation_sql(self, field: str, value: Any):
value = self._get_value_sql(value)
if value == "NULL":
return f"{self._table_name}.{field} IS NULL"
return f"{self._table_name}.{field} = {value}"
return f"{field} IS NULL"
return f"{field} = {value}"
def _build_condition(self, db_name: str, operator: str, value: Any) -> str:
"""
@ -665,13 +530,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if isinstance(attr, property):
attr = attr.fget.__name__
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
sort_clauses.extend(
self._build_foreign_order_by(foreign_table, direction)
)
continue
match attr:
case "createdUtc":
attr = "created"
@ -689,30 +547,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return ", ".join(sort_clauses)
def _build_foreign_order_by(self, table: str, direction: str) -> list[str]:
"""
Build SQL order by clause for foreign key references
:param table: Foreign table name
:param direction: Sort direction
:return: List of order by clauses
"""
sort_clauses = []
for attr, sub_direction in direction.items():
if isinstance(attr, property):
attr = attr.fget.__name__
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
sort_clauses.extend(
self._build_foreign_order_by(foreign_table, sub_direction)
)
continue
db_name = f"{table}.{attr.lower().replace('_', '')}"
sort_clauses.append(f"{db_name} {sub_direction.upper()}")
return sort_clauses
@staticmethod
async def _get_editor_id(obj: T_DBM):
editor_id = obj.editor_id

View File

@ -1,8 +1,2 @@
import re
def first_to_lower(s: str) -> str:
return s[0].lower() + s[1:] if s else s
def camel_to_snake(s: str) -> str:
return re.sub(r'(?<!^)(?=[A-Z])', '_', s).lower()

View File

@ -7,9 +7,6 @@ class Permissions(Enum):
"""
Administration
"""
# administrator
administrator = "administrator"
# api keys
api_keys = "api_keys"
api_keys_create = "api_keys.create"
@ -22,10 +19,6 @@ class Permissions(Enum):
users_update = "users.update"
users_delete = "users.delete"
# settings
settings = "settings"
settings_update = "settings.update"
"""
Permissions
"""

View File

@ -1,26 +0,0 @@
{
"rules": {
"keysOnViews": "error",
"zombieKeys": "error",
"misprintKeys": "disable",
"deepSearch": "enable",
"emptyKeys": "warning",
"maxWarning": "0",
"misprintCoefficient": "0.9",
"ignoredKeys": [
"permissions.*",
"permission_descriptions.*"
],
"ignoredMisprintKeys": [],
"customRegExpToFindKeys": [
"(?<=countHeaderTranslation=\")[A-Za-z0-9_.-]+(?=\")",
"(?<=translationKey:\\s*['\"])[A-Za-z0-9_.-]+(?=['\"])",
"(?<=(success|info|warn|error)\\(['\"])[A-Za-z0-9_.-]+(?=['\"]\\))",
"(?<=instant\\(['\"])[A-Za-z0-9_.-]+(?=['\"]\\))",
"(?<=\\.instant\\(['\"])[A-Za-z0-9_.-]+(?=['\"]\\))|(?<=\\?\\s*['\"])[A-Za-z0-9_.-]+(?=['\"]\\s*:\\s*['\"].*?\\|\\s*translate)|(?<=:\\s*['\"])[A-Za-z0-9_.-]+(?=['\"]\\s*\\|\\s*translate)\n"
]
},
"fixZombiesKeys": false,
"project": "./src/app/**/*.{html,ts}",
"languages": "./src/assets/i18n/*.json"
}

209
web/package-lock.json generated
View File

@ -21,7 +21,6 @@
"apollo-angular": "^7.2.1",
"date-fns": "^4.1.0",
"dompurify": "^3.2.1",
"graphql-ws": "^5.16.2",
"keycloak-angular": "^16.1.0",
"keycloak-js": "^26.0.5",
"marked": "^12.0.2",
@ -58,7 +57,6 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"ngx-translate-lint": "^1.22.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prettier-eslint": "^16.3.0",
@ -7379,13 +7377,6 @@
"node": ">= 0.6"
}
},
"node_modules/conventional-cli": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/conventional-cli/-/conventional-cli-1.2.0.tgz",
"integrity": "sha512-4EGXbt16iIOjTz7ocOInsHfjxL6NxdUNqnHv4XHxXfRc8ClZJcQB5SoxQhT7U2XmZ9y2O/PFFkT8hLwG3n+DJg==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
@ -9720,13 +9711,6 @@
"node": ">= 0.6"
}
},
"node_modules/fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==",
"dev": true,
"license": "ISC"
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@ -10012,18 +9996,6 @@
"graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
}
},
"node_modules/graphql-ws": {
"version": "5.16.2",
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz",
"integrity": "sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"graphql": ">=0.11 <=16"
}
},
"node_modules/hachure-fill": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
@ -12863,141 +12835,6 @@
"zone.js": "~0.14.0"
}
},
"node_modules/ngx-translate-lint": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/ngx-translate-lint/-/ngx-translate-lint-1.22.0.tgz",
"integrity": "sha512-7ECu8xs5OTWvJ6/9JC6CVhxooqRopGm6LO4BW9VhPQNFQJKuE13bipBxtW3jGz9ecyTVJuQ3hIVDG/8uSAkyig==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^2.4.2",
"commander": "^2.20.0",
"conventional-cli": "^1.2.0",
"dir-glob": "^3.0.1",
"fs": "0.0.1-security",
"glob": "^7.1.4",
"lodash": "^4.17.20",
"path": "^0.12.7",
"rxjs": "^6.5.4",
"string-similarity": "^4.0.1",
"typescript": "^4.1.2"
},
"bin": {
"ngx-translate-lint": "dist/bin.js"
}
},
"node_modules/ngx-translate-lint/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/ngx-translate-lint/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/ngx-translate-lint/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/ngx-translate-lint/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true,
"license": "MIT"
},
"node_modules/ngx-translate-lint/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/ngx-translate-lint/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ngx-translate-lint/node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
}
},
"node_modules/ngx-translate-lint/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/ngx-translate-lint/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true,
"license": "0BSD"
},
"node_modules/ngx-translate-lint/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/nice-napi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@ -13797,17 +13634,6 @@
"node": ">= 0.8"
}
},
"node_modules/path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"node_modules/path-data-parser": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
@ -14641,16 +14467,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -16271,14 +16087,6 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-similarity": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz",
"integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dev": true,
"license": "ISC"
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@ -17211,16 +17019,6 @@
"node": ">=6"
}
},
"node_modules/util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "2.0.3"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -17228,13 +17026,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/util/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"dev": true,
"license": "ISC"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -9,8 +9,6 @@
"test": "ng test",
"test:ci": "ng test --browsers=ChromeHeadlessCustom --watch=false --code-coverage",
"lint": "ng lint",
"lint:fix": "ng lint --fix",
"lint:translations": "ngx-translate-lint -c ngx-translate-lint.json",
"prettiefy": "prettier --write \"src/**/*.ts\""
},
"private": true,
@ -28,7 +26,6 @@
"apollo-angular": "^7.2.1",
"date-fns": "^4.1.0",
"dompurify": "^3.2.1",
"graphql-ws": "^5.16.2",
"keycloak-angular": "^16.1.0",
"keycloak-js": "^26.0.5",
"marked": "^12.0.2",
@ -65,7 +62,6 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"ngx-translate-lint": "^1.22.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prettier-eslint": "^16.3.0",

View File

@ -3,7 +3,6 @@ import { RouterModule, Routes } from '@angular/router';
import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component';
import { AuthGuard } from 'src/app/core/guard/auth.guard';
import { HomeComponent } from 'src/app/components/home/home.component';
import { ServerUnavailableComponent } from 'src/app/components/error/server-unavailable/server-unavailable.component';
const routes: Routes = [
{
@ -16,12 +15,8 @@ const routes: Routes = [
import('./modules/admin/admin.module').then(m => m.AdminModule),
canActivate: [AuthGuard],
},
{ path: 'error/404', component: NotFoundComponent },
{ path: 'error/unavailable', component: ServerUnavailableComponent },
{
path: '**',
redirectTo: 'error/404',
},
{ path: '404', component: NotFoundComponent },
{ path: '**', redirectTo: '/404', pathMatch: 'full' },
];
@NgModule({

View File

@ -4,7 +4,6 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { KeycloakService } from 'keycloak-angular';
import { HomeComponent } from './components/home/home.component';
import { initializeKeycloak } from './core/init-keycloak';
import { HttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';
@ -21,8 +20,8 @@ import { DialogService } from 'primeng/dynamicdialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SidebarComponent } from './components/sidebar/sidebar.component';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ConfigService } from 'src/app/service/config.service';
import { ServerUnavailableComponent } from 'src/app/components/error/server-unavailable/server-unavailable.component';
import { HomeComponent } from './components/home/home.component';
import { SettingsService } from 'src/app/service/settings.service';
if (environment.production) {
Logger.enableProductionMode();
@ -36,13 +35,13 @@ export function HttpLoaderFactory(http: HttpClient) {
export function appInitializerFactory(
keycloak: KeycloakService,
config: ConfigService
settings: SettingsService
): () => Promise<void> {
return (): Promise<void> =>
new Promise<void>((resolve, reject) => {
config
settings
.loadSettings()
.then(() => initializeKeycloak(keycloak, config))
.then(() => initializeKeycloak(keycloak, settings))
.then(() => resolve())
.catch(error => reject(error));
});
@ -51,13 +50,12 @@ export function appInitializerFactory(
@NgModule({
declarations: [
AppComponent,
HomeComponent,
FooterComponent,
HeaderComponent,
NotFoundComponent,
ServerUnavailableComponent,
SpinnerComponent,
SidebarComponent,
HomeComponent,
],
imports: [
BrowserModule,
@ -88,7 +86,7 @@ export function appInitializerFactory(
provide: APP_INITIALIZER,
useFactory: appInitializerFactory,
multi: true,
deps: [KeycloakService, ConfigService],
deps: [KeycloakService, SettingsService],
},
{
provide: ErrorHandler,

View File

@ -1,10 +1,8 @@
<div class="w-full h-full flex flex-col justify-center items-center">
<div class="bg2 flex p-10 rounded-xl text-center">
<div class="flex flex-col gap-5">
<h1 class="flex justify-center items-center">
{{ 'error.404' | translate }}
</h1>
<img src="/assets/not_found.gif" alt="" />
</div>
<div class="bg-2 padding-10 rounded-15">
<h1 class="flex justify-center items-center">
{{ 'error.404' | translate }}
</h1>
<img src="/assets/not_found.gif" alt="" />
</div>
</div>

View File

@ -1,9 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component';
import { TranslateModule } from '@ngx-translate/core';
import { NotFoundComponent } from "src/app/components/error/not-found/not-found.component";
import { TranslateModule } from "@ngx-translate/core";
describe('NotFoundComponent', () => {
describe("NotFoundComponent", () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
@ -20,7 +20,7 @@ describe('NotFoundComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
@Component({
selector: 'app-not-found',
templateUrl: './not-found.component.html',
styleUrls: ['./not-found.component.scss'],
selector: "app-not-found",
templateUrl: "./not-found.component.html",
styleUrls: ["./not-found.component.scss"],
})
export class NotFoundComponent {}

View File

@ -1,12 +0,0 @@
<div class="w-full h-full flex flex-col justify-center items-center">
<div class="bg2 flex p-10 rounded-xl text-center">
<div class="flex flex-col gap-5">
<h1 class="flex justify-center items-center">
{{ 'error.server_unavailable' | translate }}
</h1>
<p-button (onClick)="retryConnection()" class="btn btn-primary">
{{ 'error.retry' | translate }}
</p-button>
</div>
</div>
</div>

View File

@ -1,4 +0,0 @@
h1 {
color: #a03033;
font-size: 3rem !important;
}

View File

@ -1,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component';
import { TranslateModule } from '@ngx-translate/core';
describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NotFoundComponent],
imports: [TranslateModule.forRoot()],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NotFoundComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,15 +0,0 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-server-unavailable',
templateUrl: './server-unavailable.component.html',
styleUrls: ['./server-unavailable.component.scss'],
})
export class ServerUnavailableComponent {
constructor(private router: Router) {}
async retryConnection() {
await this.router.navigate(['/']);
}
}

View File

@ -1,23 +1,23 @@
import { Component } from '@angular/core';
import { ConfigService } from 'src/app/service/config.service';
import { Component } from "@angular/core";
import { SettingsService } from "src/app/service/settings.service";
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
selector: "app-footer",
templateUrl: "./footer.component.html",
styleUrls: ["./footer.component.scss"],
})
export class FooterComponent {
constructor(private config: ConfigService) {}
constructor(private settings: SettingsService) {}
get termsUrl(): string {
return this.config.settings.termsUrl;
return this.settings.settings.termsUrl;
}
get privacyUrl(): string {
return this.config.settings.privacyURL;
return this.settings.settings.privacyURL;
}
get imprintUrl(): string {
return this.config.settings.imprintURL;
return this.settings.settings.imprintURL;
}
}

View File

@ -9,7 +9,6 @@ import { AuthService } from 'src/app/service/auth.service';
import { MenuElement } from 'src/app/model/view/menu-element';
import { SidebarService } from 'src/app/service/sidebar.service';
import { SettingsService } from 'src/app/service/settings.service';
import { ConfigService } from 'src/app/service/config.service';
@Component({
selector: 'app-header',
@ -29,11 +28,11 @@ export class HeaderComponent implements OnInit, OnDestroy {
constructor(
private translateService: TranslateService,
private ngConfig: PrimeNGConfig,
private config: PrimeNGConfig,
private guiService: GuiService,
private auth: AuthService,
private sidebarService: SidebarService,
private config: ConfigService
private settings: SettingsService
) {
this.guiService.isMobile$
.pipe(takeUntil(this.unsubscribe$))
@ -49,7 +48,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
await this.initMenuLists();
});
this.themeList = this.config.settings.themes.map(theme => {
this.themeList = this.settings.settings.themes.map(theme => {
return {
label: theme.label,
command: () => {
@ -123,7 +122,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
this.translateService.use(lang);
this.translateService
.get('primeng')
.subscribe(res => this.ngConfig.setTranslation(res));
.subscribe(res => this.config.setTranslation(res));
}
async loadLang() {

View File

@ -1,9 +1,9 @@
import { Directive, inject } from '@angular/core';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { SpinnerService } from 'src/app/service/spinner.service';
import { FilterService } from 'src/app/service/filter.service';
import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Directive, inject } from "@angular/core";
import { PageDataService } from "src/app/core/base/page.data.service";
import { SpinnerService } from "src/app/service/spinner.service";
import { FilterService } from "src/app/service/filter.service";
import { FormGroup } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
@Directive()
export abstract class FormPageBase<
@ -27,7 +27,7 @@ export abstract class FormPageBase<
protected dataService = inject(PageDataService) as S;
protected constructor() {
const id = this.route.snapshot.params['id'];
const id = this.route.snapshot.params["id"];
this.validateRoute(id);
this.buildForm();
@ -35,18 +35,18 @@ export abstract class FormPageBase<
validateRoute(id: string | undefined) {
const url = this.router.url;
if (url.endsWith('create') && id !== undefined) {
throw new Error('Route ends with create but id is defined');
if (url.endsWith("create") && id !== undefined) {
throw new Error("Route ends with create but id is defined");
}
if (url.endsWith('edit') && (id === undefined || isNaN(+id))) {
throw new Error('Route ends with edit but id is not a number');
if (url.endsWith("edit") && (id === undefined || isNaN(+id))) {
throw new Error("Route ends with edit but id is not a number");
}
this.nodeId = id ? +id : undefined;
}
close() {
const backRoute = this.nodeId ? '../..' : '..';
const backRoute = this.nodeId ? "../.." : "..";
this.router.navigate([backRoute], { relativeTo: this.route }).then(() => {
this.filterService.onLoad.emit();

View File

@ -1,21 +1,21 @@
import { Directive, inject, OnDestroy } from '@angular/core';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { Subject } from 'rxjs';
import { Logger } from 'src/app/service/logger.service';
import { QueryResult } from 'src/app/model/entities/query-result';
import { SpinnerService } from 'src/app/service/spinner.service';
import { FilterService } from 'src/app/service/filter.service';
import { Filter } from 'src/app/model/graphql/filter/filter.model';
import { Sort } from 'src/app/model/graphql/filter/sort.model';
import { takeUntil } from 'rxjs/operators';
import { PaginatorState } from 'primeng/paginator';
import { PageColumns } from 'src/app/core/base/page.columns';
import { Directive, inject, OnDestroy } from "@angular/core";
import { PageDataService } from "src/app/core/base/page.data.service";
import { Subject } from "rxjs";
import { Logger } from "src/app/service/logger.service";
import { QueryResult } from "src/app/model/entities/query-result";
import { SpinnerService } from "src/app/service/spinner.service";
import { FilterService } from "src/app/service/filter.service";
import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from "src/app/model/graphql/filter/sort.model";
import { takeUntil } from "rxjs/operators";
import { PaginatorState } from "primeng/paginator";
import { PageColumns } from "src/app/core/base/page.columns";
import {
TableColumn,
TableRequireAnyPermissions,
} from 'src/app/modules/shared/components/table/table.model';
} from "src/app/modules/shared/components/table/table.model";
const logger = new Logger('PageBase');
const logger = new Logger("PageBase");
@Directive()
export abstract class PageBase<
@ -96,13 +96,13 @@ export abstract class PageBase<
}
columns: TableColumn<T>[] =
'get' in this.columnsService ? this.columnsService.get() : [];
"get" in this.columnsService ? this.columnsService.get() : [];
protected unsubscribe$ = new Subject<void>();
protected constructor(
useQueryParams = false,
permissions?: TableRequireAnyPermissions
permissions?: TableRequireAnyPermissions,
) {
this.subscribeToFilterService();
this.filterService.reset({
@ -110,17 +110,10 @@ export abstract class PageBase<
withHideDeleted: true,
});
this.requiredPermissions = permissions ?? {};
this.dataService
.onChange()
.pipe(takeUntil(this.unsubscribe$))
.subscribe(() => {
this.load(true);
});
}
ngOnDestroy(): void {
logger.trace('Destroy component');
logger.trace("Destroy component");
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
@ -132,26 +125,26 @@ export abstract class PageBase<
this.filterService.filter$
.pipe(takeUntil(this.unsubscribe$))
.subscribe(filter => {
.subscribe((filter) => {
this._filter = filter;
});
this.filterService.sort$
.pipe(takeUntil(this.unsubscribe$))
.subscribe(sort => {
.subscribe((sort) => {
this._sort = sort;
});
this.filterService.skip$
.pipe(takeUntil(this.unsubscribe$))
.subscribe(skip => {
.subscribe((skip) => {
this._skip = skip;
});
this.filterService.take$
.pipe(takeUntil(this.unsubscribe$))
.subscribe(take => {
if (take && Object.prototype.hasOwnProperty.call(take, 'showAll')) {
.subscribe((take) => {
if (take && Object.prototype.hasOwnProperty.call(take, "showAll")) {
this._take = 0;
return;
}
@ -170,5 +163,5 @@ export abstract class PageBase<
this.filterService.onLoad.emit();
}
abstract load(silent?: boolean): void;
abstract load(): void;
}

View File

@ -1,69 +1,67 @@
import { Injectable } from '@angular/core';
import { TableColumn } from 'src/app/modules/shared/components/table/table.model';
import { DbModel } from 'src/app/model/entities/db-model';
import { Injectable } from "@angular/core";
import { TableColumn } from "src/app/modules/shared/components/table/table.model";
import { DbModel } from "src/app/model/entities/db-model";
@Injectable({
providedIn: 'root',
providedIn: "root",
})
export abstract class PageColumns<T> {
abstract get(): TableColumn<T>[];
}
export const ID_COLUMN = {
name: 'id',
translationKey: 'common.id',
type: 'number',
name: "id",
label: "common.id",
type: "number",
filterable: true,
value: (row: { id?: number }) => row.id,
class: 'max-w-24',
class: "max-w-24",
};
export const NAME_COLUMN = {
name: 'name',
translationKey: 'common.name',
type: 'text',
name: "name",
label: "common.name",
type: "text",
filterable: true,
value: (row: { name?: string }) => row.name,
};
export const DESCRIPTION_COLUMN = {
name: 'description',
translationKey: 'common.description',
type: 'text',
name: "description",
label: "common.description",
type: "text",
filterable: true,
value: (row: { description?: string }) => row.description,
};
export const DELETED_COLUMN = {
name: 'deleted',
translationKey: 'common.deleted',
type: 'bool',
name: "deleted",
label: "common.deleted",
type: "bool",
filterable: true,
value: (row: DbModel) => row.deleted,
};
export const EDITOR_COLUMN = {
name: 'editor',
translationKey: 'common.editor',
name: "editor",
label: "common.editor",
value: (row: DbModel) => row.editor?.username,
};
export const CREATED_UTC_COLUMN = {
name: 'createdUtc',
translationKey: 'common.created',
type: 'date',
filterable: true,
name: "createdUtc",
label: "common.created",
type: "date",
value: (row: DbModel) => row.createdUtc,
class: 'max-w-32',
class: "max-w-32",
};
export const UPDATED_UTC_COLUMN = {
name: 'updatedUtc',
translationKey: 'common.updated',
type: 'date',
filterable: true,
name: "updatedUtc",
label: "common.updated",
type: "date",
value: (row: DbModel) => row.updatedUtc,
class: 'max-w-32',
class: "max-w-32",
};
export const DB_MODEL_COLUMNS = [

View File

@ -1,24 +1,22 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { MutationResult } from 'apollo-angular';
import { Filter } from 'src/app/model/graphql/filter/filter.model';
import { Sort } from 'src/app/model/graphql/filter/sort.model';
import { QueryResult } from 'src/app/model/entities/query-result';
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { MutationResult } from "apollo-angular";
import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from "src/app/model/graphql/filter/sort.model";
import { QueryResult } from "src/app/model/entities/query-result";
@Injectable({
providedIn: 'root',
providedIn: "root",
})
export abstract class PageDataService<T> {
abstract load(
filter?: Filter[],
sort?: Sort[],
skip?: number,
take?: number
take?: number,
): Observable<QueryResult<T>>;
abstract loadById(id: number): Observable<T>;
abstract onChange(): Observable<void>;
}
export interface Create<T, C> {
@ -31,12 +29,12 @@ export interface Update<T, U> {
export interface Delete<T> {
delete(
object: T
object: T,
): Observable<T | undefined | boolean> | Observable<MutationResult>;
}
export interface Restore<T> {
restore(
object: T
object: T,
): Observable<T | undefined | boolean> | Observable<MutationResult>;
}

View File

@ -1,15 +1,15 @@
import { KeycloakService } from 'keycloak-angular';
import { ConfigService } from 'src/app/service/config.service';
import { SettingsService } from 'src/app/service/settings.service';
export function initializeKeycloak(
keycloak: KeycloakService,
config: ConfigService
settings: SettingsService
): Promise<boolean> {
return keycloak.init({
config: {
url: config.settings.keycloak.url,
realm: config.settings.keycloak.realm,
clientId: config.settings.keycloak.clientId,
url: settings.settings.keycloak.url,
realm: settings.settings.keycloak.realm,
clientId: settings.settings.keycloak.clientId,
},
initOptions: {
onLoad: 'check-sso',

View File

@ -1,7 +1,5 @@
export enum PermissionsEnum {
// Administration
administrator = 'administrator',
apiKeys = 'api_keys',
apiKeysCreate = 'api_keys.create',
apiKeysUpdate = 'api_keys.update',

View File

@ -23,6 +23,5 @@ export interface KeycloakSettings {
export interface ApiSettings {
url: string;
wsUrl: string;
redirector: string;
}

View File

@ -1,4 +0,0 @@
export interface FeatureFlag {
key: string;
value: boolean;
}

View File

@ -1,4 +0,0 @@
export interface Setting {
key: string;
value: string;
}

View File

@ -1,61 +1,41 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { PermissionGuard } from 'src/app/core/guard/permission.guard';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { SharedModule } from "src/app/modules/shared/shared.module";
const routes: Routes = [
{
path: 'users',
title: 'Users | Maxlan',
path: "users",
title: "Users | Maxlan",
loadChildren: () =>
import('src/app/modules/admin/administration/users/users.module').then(
m => m.UsersModule
import("src/app/modules/admin/administration/users/users.module").then(
(m) => m.UsersModule,
),
canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.users] },
},
{
path: 'roles',
title: 'Roles | Maxlan',
path: "roles",
title: "Roles | Maxlan",
loadChildren: () =>
import('src/app/modules/admin/administration/roles/roles.module').then(
m => m.RolesModule
import("src/app/modules/admin/administration/roles/roles.module").then(
(m) => m.RolesModule,
),
canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.roles] },
},
{
path: 'api-keys',
title: 'API Key | Maxlan',
path: "api-keys",
title: "API Key | Maxlan",
loadChildren: () =>
import(
'src/app/modules/admin/administration/api-keys/api-keys.module'
).then(m => m.ApiKeysModule),
"src/app/modules/admin/administration/api-keys/api-keys.module"
).then((m) => m.ApiKeysModule),
canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.apiKeys] },
},
{
path: 'feature-flags',
title: 'Feature-flags | Maxlan',
loadChildren: () =>
import(
'src/app/modules/admin/administration/feature-flags/feature-flags.module'
).then(m => m.FeatureFlagsModule),
canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.administrator] },
},
{
path: 'settings',
title: 'Settings | Maxlan',
loadChildren: () =>
import(
'src/app/modules/admin/administration/settings/settings.module'
).then(m => m.SettingsModule),
canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.administrator] },
},
];
@NgModule({

View File

@ -1,11 +1,11 @@
import { Injectable, Provider } from '@angular/core';
import { Injectable, Provider } from "@angular/core";
import {
DB_MODEL_COLUMNS,
ID_COLUMN,
PageColumns,
} from 'src/app/core/base/page.columns';
import { TableColumn } from 'src/app/modules/shared/components/table/table.model';
import { ApiKey } from 'src/app/model/entities/api-key';
} from "src/app/core/base/page.columns";
import { TableColumn } from "src/app/modules/shared/components/table/table.model";
import { ApiKey } from "src/app/model/entities/api-key";
@Injectable()
export class ApiKeysColumns extends PageColumns<ApiKey> {
@ -13,16 +13,16 @@ export class ApiKeysColumns extends PageColumns<ApiKey> {
return [
ID_COLUMN,
{
name: 'identifier',
translationKey: 'common.identifier',
type: 'text',
name: "identifier",
label: "common.identifier",
type: "text",
filterable: true,
value: (row: ApiKey) => row.identifier,
},
{
name: 'key',
translationKey: 'common.key',
type: 'password',
name: "key",
label: "common.key",
type: "password",
value: (row: ApiKey) => row.key,
},
...DB_MODEL_COLUMNS,

View File

@ -1,25 +1,25 @@
import { Injectable, Provider } from '@angular/core';
import { Observable } from 'rxjs';
import { Injectable, Provider } from "@angular/core";
import { Observable } from "rxjs";
import {
Create,
Delete,
PageDataService,
Restore,
Update,
} from 'src/app/core/base/page.data.service';
import { Permission } from 'src/app/model/entities/role';
import { Filter } from 'src/app/model/graphql/filter/filter.model';
import { Sort } from 'src/app/model/graphql/filter/sort.model';
import { Apollo, gql } from 'apollo-angular';
import { QueryResult } from 'src/app/model/entities/query-result';
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query';
import { catchError, map } from 'rxjs/operators';
import { SpinnerService } from 'src/app/service/spinner.service';
} from "src/app/core/base/page.data.service";
import { Permission } from "src/app/model/entities/role";
import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from "src/app/model/graphql/filter/sort.model";
import { Apollo, gql } from "apollo-angular";
import { QueryResult } from "src/app/model/entities/query-result";
import { DB_MODEL_FRAGMENT } from "src/app/model/graphql/db-model.query";
import { catchError, map } from "rxjs/operators";
import { SpinnerService } from "src/app/service/spinner.service";
import {
ApiKey,
ApiKeyCreateInput,
ApiKeyUpdateInput,
} from 'src/app/model/entities/api-key';
} from "src/app/model/entities/api-key";
@Injectable()
export class ApiKeysDataService
@ -32,7 +32,7 @@ export class ApiKeysDataService
{
constructor(
private spinner: SpinnerService,
private apollo: Apollo
private apollo: Apollo,
) {
super();
}
@ -41,7 +41,7 @@ export class ApiKeysDataService
filter?: Filter[] | undefined,
sort?: Sort[] | undefined,
skip?: number | undefined,
take?: number | undefined
take?: number | undefined,
): Observable<QueryResult<ApiKey>> {
return this.apollo
.query<{ apiKeys: QueryResult<ApiKey> }>({
@ -78,12 +78,12 @@ export class ApiKeysDataService
},
})
.pipe(
catchError(err => {
catchError((err) => {
this.spinner.hide();
throw err;
})
}),
)
.pipe(map(result => result.data.apiKeys));
.pipe(map((result) => result.data.apiKeys));
}
loadById(id: number): Observable<ApiKey> {
@ -109,24 +109,12 @@ export class ApiKeysDataService
},
})
.pipe(
catchError(err => {
catchError((err) => {
this.spinner.hide();
throw err;
})
}),
)
.pipe(map(result => result.data.apiKeys.nodes[0]));
}
onChange(): Observable<void> {
return this.apollo
.subscribe<{ apiKeyChange: void }>({
query: gql`
subscription onApiKeyChange {
apiKeyChange
}
`,
})
.pipe(map(result => result.data?.apiKeyChange));
.pipe(map((result) => result.data.apiKeys.nodes[0]));
}
create(object: ApiKeyCreateInput): Observable<ApiKey | undefined> {
@ -152,17 +140,17 @@ export class ApiKeysDataService
variables: {
input: {
identifier: object.identifier,
permissions: object.permissions?.map(x => x.id),
permissions: object.permissions?.map((x) => x.id),
},
},
})
.pipe(
catchError(err => {
catchError((err) => {
this.spinner.hide();
throw err;
})
}),
)
.pipe(map(result => result.data?.apiKey.create));
.pipe(map((result) => result.data?.apiKey.create));
}
update(object: ApiKeyUpdateInput): Observable<ApiKey | undefined> {
@ -189,17 +177,17 @@ export class ApiKeysDataService
input: {
id: object.id,
identifier: object.identifier,
permissions: object.permissions?.map(x => x.id),
permissions: object.permissions?.map((x) => x.id),
},
},
})
.pipe(
catchError(err => {
catchError((err) => {
this.spinner.hide();
throw err;
})
}),
)
.pipe(map(result => result.data?.apiKey.update));
.pipe(map((result) => result.data?.apiKey.update));
}
delete(object: ApiKey): Observable<boolean> {
@ -217,12 +205,12 @@ export class ApiKeysDataService
},
})
.pipe(
catchError(err => {
catchError((err) => {
this.spinner.hide();
throw err;
})
}),
)
.pipe(map(result => result.data?.apiKey.delete ?? false));
.pipe(map((result) => result.data?.apiKey.delete ?? false));
}
restore(object: ApiKey): Observable<boolean> {
@ -240,12 +228,12 @@ export class ApiKeysDataService
},
})
.pipe(
catchError(err => {
catchError((err) => {
this.spinner.hide();
throw err;
})
}),
)
.pipe(map(result => result.data?.apiKey.restore ?? false));
.pipe(map((result) => result.data?.apiKey.restore ?? false));
}
getAllPermissions(): Observable<Permission[]> {
@ -263,12 +251,12 @@ export class ApiKeysDataService
`,
})
.pipe(
catchError(err => {
catchError((err) => {
this.spinner.hide();
throw err;
})
}),
)
.pipe(map(result => result.data.permissions.nodes));
.pipe(map((result) => result.data.permissions.nodes));
}
static provide(): Provider[] {

View File

@ -1,22 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { PermissionGuard } from 'src/app/core/guard/permission.guard';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { ApiKeyFormPageComponent } from 'src/app/modules/admin/administration/api-keys/form-page/api-key-form-page.component';
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page';
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
import { ApiKeysColumns } from 'src/app/modules/admin/administration/api-keys/api-keys.columns';
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { ApiKeyFormPageComponent } from "src/app/modules/admin/administration/api-keys/form-page/api-key-form-page.component";
import { ApiKeysPage } from "src/app/modules/admin/administration/api-keys/api-keys.page";
import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { ApiKeysColumns } from "src/app/modules/admin/administration/api-keys/api-keys.columns";
const routes: Routes = [
{
path: '',
title: 'Admin - ApiKeys | Maxlan',
path: "",
title: "Admin - ApiKeys | Maxlan",
component: ApiKeysPage,
children: [
{
path: 'create',
path: "create",
component: ApiKeyFormPageComponent,
canActivate: [PermissionGuard],
data: {
@ -24,7 +24,7 @@ const routes: Routes = [
},
},
{
path: 'edit/:id',
path: "edit/:id",
component: ApiKeyFormPageComponent,
canActivate: [PermissionGuard],
data: {

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ApiKeysPage } from "src/app/modules/admin/administration/api-keys/api-keys.page";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { PageDataService } from "src/app/core/base/page.data.service";
import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
describe('ApiKeysComponent', () => {
describe("ApiKeysComponent", () => {
let component: ApiKeysPage;
let fixture: ComponentFixture<ApiKeysPage>;
@ -45,7 +45,7 @@ describe('ApiKeysComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,16 +1,16 @@
import { Component } from '@angular/core';
import { PageBase } from 'src/app/core/base/page-base';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { ApiKey } from 'src/app/model/entities/api-key';
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
import { ApiKeysColumns } from 'src/app/modules/admin/administration/api-keys/api-keys.columns';
import { Component } from "@angular/core";
import { PageBase } from "src/app/core/base/page-base";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { ApiKey } from "src/app/model/entities/api-key";
import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { ApiKeysColumns } from "src/app/modules/admin/administration/api-keys/api-keys.columns";
@Component({
selector: 'app-api-keys',
templateUrl: './api-keys.page.html',
styleUrl: './api-keys.page.scss',
selector: "app-api-keys",
templateUrl: "./api-keys.page.html",
styleUrl: "./api-keys.page.scss",
})
export class ApiKeysPage extends PageBase<
ApiKey,
@ -19,7 +19,7 @@ export class ApiKeysPage extends PageBase<
> {
constructor(
private toast: ToastService,
private confirmation: ConfirmationDialogService
private confirmation: ConfirmationDialogService,
) {
super(true, {
read: [PermissionsEnum.apiKeys],
@ -30,11 +30,11 @@ export class ApiKeysPage extends PageBase<
});
}
load(silent?: boolean): void {
if (silent) this.loading = true;
load(): void {
this.loading = true;
this.dataService
.load(this.filter, this.sort, this.skip, this.take)
.subscribe(result => {
.subscribe((result) => {
this.result = result;
this.loading = false;
});
@ -42,12 +42,12 @@ export class ApiKeysPage extends PageBase<
delete(apiKey: ApiKey): void {
this.confirmation.confirmDialog({
header: 'dialog.delete.header',
message: 'dialog.delete.message',
header: "dialog.delete.header",
message: "dialog.delete.message",
accept: () => {
this.loading = true;
this.dataService.delete(apiKey).subscribe(() => {
this.toast.success('action.deleted');
this.toast.success("action.deleted");
this.load();
});
},
@ -57,12 +57,12 @@ export class ApiKeysPage extends PageBase<
restore(apiKey: ApiKey): void {
this.confirmation.confirmDialog({
header: 'dialog.restore.header',
message: 'dialog.restore.message',
header: "dialog.restore.header",
message: "dialog.restore.message",
accept: () => {
this.loading = true;
this.dataService.restore(apiKey).subscribe(() => {
this.toast.success('action.restored');
this.toast.success("action.restored");
this.load();
});
},

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('ApiKeyFormpageComponent', () => {
describe("ApiKeyFormpageComponent", () => {
let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('ApiKeyFormpageComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,21 +1,21 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { InputSwitchChangeEvent } from 'primeng/inputswitch';
import { ToastService } from 'src/app/service/toast.service';
import { firstValueFrom } from 'rxjs';
import { FormPageBase } from 'src/app/core/base/form-page-base';
import { Permission } from 'src/app/model/entities/role';
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { InputSwitchChangeEvent } from "primeng/inputswitch";
import { ToastService } from "src/app/service/toast.service";
import { firstValueFrom } from "rxjs";
import { FormPageBase } from "src/app/core/base/form-page-base";
import { Permission } from "src/app/model/entities/role";
import {
ApiKey,
ApiKeyCreateInput,
ApiKeyUpdateInput,
} from 'src/app/model/entities/api-key';
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
} from "src/app/model/entities/api-key";
import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
@Component({
selector: 'app-api-key-form-page',
templateUrl: './api-key-form-page.component.html',
styleUrl: './api-key-form-page.component.scss',
selector: "app-api-key-form-page",
templateUrl: "./api-key-form-page.component.html",
styleUrl: "./api-key-form-page.component.scss",
})
export class ApiKeyFormPageComponent extends FormPageBase<
ApiKey,
@ -38,7 +38,7 @@ export class ApiKeyFormPageComponent extends FormPageBase<
this.dataService
.load([{ id: { equal: this.nodeId } }])
.subscribe(apiKey => {
.subscribe((apiKey) => {
this.node = apiKey.nodes[0];
this.setForm(this.node);
});
@ -47,12 +47,12 @@ export class ApiKeyFormPageComponent extends FormPageBase<
async initializePermissions() {
const permissions = await firstValueFrom(
this.dataService.getAllPermissions()
this.dataService.getAllPermissions(),
);
this.allPermissions = permissions;
this.permissionGroups = permissions.reduce(
(acc, p) => {
const group = p.name.includes('.') ? p.name.split('.')[0] : p.name;
const group = p.name.includes(".") ? p.name.split(".")[0] : p.name;
if (!acc[group]) {
acc[group] = [];
@ -60,10 +60,10 @@ export class ApiKeyFormPageComponent extends FormPageBase<
acc[group].push(p);
return acc;
},
{} as { [key: string]: Permission[] }
{} as { [key: string]: Permission[] },
);
permissions.forEach(p => {
permissions.forEach((p) => {
this.form.addControl(p.name, new FormControl<boolean>(false));
});
}
@ -77,48 +77,48 @@ export class ApiKeyFormPageComponent extends FormPageBase<
id: new FormControl<number | undefined>(undefined),
identifier: new FormControl<string | undefined>(
undefined,
Validators.required
Validators.required,
),
});
this.form.controls['id'].disable();
this.form.controls["id"].disable();
}
setForm(node?: ApiKey) {
this.form.controls['id'].setValue(node?.id);
this.form.controls['identifier'].setValue(node?.identifier);
this.form.controls["id"].setValue(node?.id);
this.form.controls["identifier"].setValue(node?.identifier);
if (!node) return;
const permissions = node.permissions ?? [];
permissions.forEach(p => {
permissions.forEach((p) => {
this.form.controls[p.name].setValue(true);
});
}
getCreateInput(): ApiKeyCreateInput {
return {
identifier: this.form.controls['identifier'].pristine
identifier: this.form.controls["identifier"].pristine
? undefined
: (this.form.controls['identifier'].value ?? undefined),
: (this.form.controls["identifier"].value ?? undefined),
permissions: this.allPermissions.filter(
p => this.form.controls[p.name].value
(p) => this.form.controls[p.name].value,
),
};
}
getUpdateInput(): ApiKeyUpdateInput {
if (!this.node?.id) {
throw new Error('Node id is missing');
throw new Error("Node id is missing");
}
return {
id: this.form.controls['id'].value,
identifier: this.form.controls['identifier'].pristine
id: this.form.controls["id"].value,
identifier: this.form.controls["identifier"].pristine
? undefined
: (this.form.controls['identifier'].value ?? undefined),
: (this.form.controls["identifier"].value ?? undefined),
permissions: this.allPermissions.filter(
p => this.form.controls[p.name].value
(p) => this.form.controls[p.name].value,
),
};
}
@ -126,7 +126,7 @@ export class ApiKeyFormPageComponent extends FormPageBase<
create(apiKey: ApiKeyCreateInput): void {
this.dataService.create(apiKey).subscribe(() => {
this.spinner.hide();
this.toast.success('action.created');
this.toast.success("action.created");
this.close();
});
}
@ -134,20 +134,20 @@ export class ApiKeyFormPageComponent extends FormPageBase<
update(apiKey: ApiKeyUpdateInput): void {
this.dataService.update(apiKey).subscribe(() => {
this.spinner.hide();
this.toast.success('action.updated');
this.toast.success("action.created");
this.close();
});
}
toggleGroup(event: InputSwitchChangeEvent, group: string) {
this.permissionGroups[group].forEach(p => {
this.permissionGroups[group].forEach((p) => {
this.form.controls[p.name].setValue(event.checked);
});
}
isGroupChecked(group: string) {
return this.permissionGroups[group].every(
p => this.form.controls[p.name].value
(p) => this.form.controls[p.name].value,
);
}

View File

@ -1,105 +0,0 @@
import { Injectable, Provider } from '@angular/core';
import { Observable } from 'rxjs';
import { Apollo, gql } from 'apollo-angular';
import { catchError, map } from 'rxjs/operators';
import { SpinnerService } from 'src/app/service/spinner.service';
import { FeatureFlag } from 'src/app/model/entities/feature-flag';
@Injectable()
export class FeatureFlagsDataService {
constructor(
private spinner: SpinnerService,
private apollo: Apollo
) {}
load(): Observable<FeatureFlag[]> {
return this.apollo
.query<{ featureFlags: FeatureFlag[] }>({
query: gql`
query GetFeatureFlags {
featureFlags {
key
value
}
}
`,
variables: {},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data.featureFlags));
}
loadByKey(key: string): Observable<FeatureFlag> {
return this.apollo
.query<{ featureFlags: FeatureFlag[] }>({
query: gql`
query getFeatureFlag($key: String!) {
featureFlag(key: $key) {
key
value
}
}
`,
variables: {
key,
},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data.featureFlags[0]));
}
onUpdate(): Observable<void> {
return this.apollo
.subscribe<{ featureFlagChange: void }>({
query: gql`
subscription onFeatureFlagChange {
featureFlagChange
}
`,
})
.pipe(map(result => result.data?.featureFlagChange));
}
change(object: FeatureFlag): Observable<FeatureFlag | undefined> {
return this.apollo
.mutate<{ featureFlag: { change: FeatureFlag } }>({
mutation: gql`
mutation changeFeatureFlag($input: FeatureFlagInput!) {
featureFlag {
change(input: $input) {
key
value
}
}
}
`,
variables: {
input: {
key: object.key,
value: object.value,
},
},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data?.featureFlag.change));
}
static provide(): Provider[] {
return [FeatureFlagsDataService];
}
}

View File

@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { FeatureFlagsPage } from 'src/app/modules/admin/administration/feature-flags/feature-flags.page';
import { FeatureFlagsDataService } from 'src/app/modules/admin/administration/feature-flags/feature-flags.data.service';
const routes: Routes = [
{
path: '',
title: 'Admin - Feature-flags | Maxlan',
component: FeatureFlagsPage,
},
];
@NgModule({
declarations: [FeatureFlagsPage],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
providers: [FeatureFlagsDataService.provide()],
})
export class FeatureFlagsModule {}

View File

@ -1,10 +0,0 @@
<div class="flex flex-col gap-2">
<div class="grid grid-cols-4 gap-1" *ngFor="let flag of flags">
<div><h2>{{ flag.key }}</h2></div>
<div>
<p-inputSwitch
[ngModel]="flag.value"
(onChange)="change(flag, $event)"></p-inputSwitch>
</div>
</div>
</div>

View File

@ -1,47 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FeatureFlagsPage } from 'src/app/modules/admin/administration/feature-flags/feature-flags.page';
import { FeatureFlagsDataService } from 'src/app/modules/admin/administration/feature-flags/feature-flags.data.service';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
describe('FeatureFlagsComponent', () => {
let component: FeatureFlagsPage;
let fixture: ComponentFixture<FeatureFlagsPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FeatureFlagsPage],
imports: [SharedModule, TranslateModule.forRoot()],
providers: [
AuthService,
KeycloakService,
ErrorHandlingService,
ToastService,
MessageService,
ConfirmationService,
{
provide: ActivatedRoute,
useValue: {
snapshot: { params: of({}) },
},
},
FeatureFlagsDataService,
],
}).compileComponents();
fixture = TestBed.createComponent(FeatureFlagsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,71 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FeatureFlagsDataService } from 'src/app/modules/admin/administration/feature-flags/feature-flags.data.service';
import { FeatureFlag } from 'src/app/model/entities/feature-flag';
import { SpinnerService } from 'src/app/service/spinner.service';
import { catchError, takeUntil } from 'rxjs/operators';
import { InputSwitchChangeEvent } from 'primeng/inputswitch';
import { Subject } from 'rxjs';
@Component({
selector: 'app-feature-flags',
templateUrl: './feature-flags.page.html',
styleUrl: './feature-flags.page.scss',
})
export class FeatureFlagsPage implements OnInit, OnDestroy {
flags: FeatureFlag[] = [];
protected unsubscribe$ = new Subject<void>();
constructor(
private spinner: SpinnerService,
private data: FeatureFlagsDataService
) {}
ngOnInit() {
this.data
.onUpdate()
.pipe(takeUntil(this.unsubscribe$))
.subscribe(() => {
this.load();
});
this.spinner.show();
this.load();
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
load() {
this.data
.load()
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.subscribe(flags => {
this.flags = flags;
this.spinner.hide();
});
}
change(flag: FeatureFlag, event: InputSwitchChangeEvent) {
flag.value = event.checked;
this.spinner.show();
this.data
.change(flag)
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.subscribe(() => {
this.spinner.hide();
});
}
}

View File

@ -1,73 +1,73 @@
<app-form-page
*ngIf="node"
[formGroup]="form"
[isUpdate]="isUpdate"
(onSave)="save()"
(onClose)="close()">
<ng-template formPageHeader let-isUpdate>
<h2>
{{ 'common.role' | translate }}
{{
(isUpdate ? 'sidebar.header.update' : 'sidebar.header.create')
| translate
}}
</h2>
</ng-template>
*ngIf="node"
[formGroup]="form"
[isUpdate]="isUpdate"
(onSave)="save()"
(onClose)="close()">
<ng-template formPageHeader let-isUpdate>
<h2>
{{ 'common.role' | translate }}
{{
(isUpdate ? 'sidebar.header.update' : 'sidebar.header.create')
| translate
}}
</h2>
</ng-template>
<ng-template formPageContent>
<div class="form-page-input">
<p class="label">{{ 'common.id' | translate }}</p>
<input pInputText class="value" type="number" formControlName="id"/>
</div>
<div class="form-page-input">
<p class="label">{{ 'common.name' | translate }}</p>
<input pInputText class="value" type="text" formControlName="name"/>
</div>
<div class="form-page-input">
<p class="label">{{ 'common.description' | translate }}</p>
<input
pInputText
class="value"
type="text"
formControlName="description"/>
</div>
<div class="divider"></div>
<div class="flex flex-col gap-5">
<div *ngFor="let group of Object.keys(permissionGroups)">
<div class="flex flex-col gap-2">
<div class="flex justify-between">
<label class="flex-1" for="roles.permission_groups.{{ group }}">
<h3>
{{ 'permissions.' + group | translate }}
</h3>
</label>
<form #form="ngForm">
<p-inputSwitch
name="roles.permission_groups.{{ group }}"
(onChange)="toggleGroup($event, group)"
[ngModel]="isGroupChecked(group)"></p-inputSwitch>
</form>
</div>
<div
*ngFor="let permission of permissionGroups[group]"
class="flex flex-col">
<div class="flex items-center justify-between w-full">
<label class="flex-1" for="{{ permission.name }}">
{{ 'permissions.' + permission.name | translate }}
</label>
<p-inputSwitch class="flex items-center justify-center" [formControlName]="permission.name">
</p-inputSwitch>
</div>
<div *ngIf="permission.description">
<p class="text-sm text-gray-500">
{{ permission.description }}
</p>
</div>
</div>
<ng-template formPageContent>
<div class="form-page-input">
<p class="label">{{ 'common.id' | translate }}</p>
<input pInputText class="value" type="number" formControlName="id"/>
</div>
</div>
</div>
</ng-template>
<div class="form-page-input">
<p class="label">{{ 'common.name' | translate }}</p>
<input pInputText class="value" type="text" formControlName="name"/>
</div>
<div class="form-page-input">
<p class="label">{{ 'common.description' | translate }}</p>
<input
pInputText
class="value"
type="text"
formControlName="description"/>
</div>
<div class="divider"></div>
<div class="flex flex-col gap-5">
<div *ngFor="let group of Object.keys(permissionGroups)">
<div class="flex flex-col gap-2">
<div class="flex justify-between">
<label class="flex-1" for="roles.permission_groups.{{ group }}">
<h3>
{{ 'permissions.' + group | translate }}
</h3>
</label>
<form #form="ngForm">
<p-inputSwitch
name="roles.permission_groups.{{ group }}"
(onChange)="toggleGroup($event, group)"
[ngModel]="isGroupChecked(group)"></p-inputSwitch>
</form>
</div>
<div
*ngFor="let permission of permissionGroups[group]"
class="flex flex-col">
<div class="flex items-center justify-between w-full">
<label class="flex-1" for="{{ permission.name }}">
{{ 'permissions.' + permission.name | translate }}
</label>
<p-inputSwitch class="flex items-center justify-center" [formControlName]="permission.name">
</p-inputSwitch>
</div>
<div *ngIf="permission.description">
<p class="text-sm text-gray-500">
{{ permission.description }}
</p>
</div>
</div>
</div>
</div>
</div>
</ng-template>
</app-form-page>

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('RoleFormpageComponent', () => {
describe("RoleFormpageComponent", () => {
let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('RoleFormpageComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -142,7 +142,7 @@ export class RoleFormPageComponent extends FormPageBase<
update(role: RoleUpdateInput): void {
this.dataService.update(role).subscribe(() => {
this.spinner.hide();
this.toast.success('action.updated');
this.toast.success('action.created');
this.close();
});
}

View File

@ -1,13 +1,13 @@
import { Injectable, Provider } from '@angular/core';
import { Role } from 'src/app/model/entities/role';
import { Injectable, Provider } from "@angular/core";
import { Role } from "src/app/model/entities/role";
import {
DB_MODEL_COLUMNS,
DESCRIPTION_COLUMN,
ID_COLUMN,
NAME_COLUMN,
PageColumns,
} from 'src/app/core/base/page.columns';
import { TableColumn } from 'src/app/modules/shared/components/table/table.model';
} from "src/app/core/base/page.columns";
import { TableColumn } from "src/app/modules/shared/components/table/table.model";
@Injectable()
export class RolesColumns extends PageColumns<Role> {

View File

@ -120,18 +120,6 @@ export class RolesDataService
.pipe(map(result => result.data.roles.nodes[0]));
}
onChange(): Observable<void> {
return this.apollo
.subscribe<{ roleChange: void }>({
query: gql`
subscription onRoleChange {
roleChange
}
`,
})
.pipe(map(result => result.data?.roleChange));
}
create(object: RoleCreateInput): Observable<Role | undefined> {
return this.apollo
.mutate<{ role: { create: Role } }>({
@ -264,6 +252,7 @@ export class RolesDataService
nodes {
id
name
description
}
}
}

View File

@ -1,22 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { PermissionGuard } from 'src/app/core/guard/permission.guard';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component';
import { RolesPage } from 'src/app/modules/admin/administration/roles/roles.page';
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service';
import { RolesColumns } from 'src/app/modules/admin/administration/roles/roles.columns';
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { RolesPage } from "src/app/modules/admin/administration/roles/roles.page";
import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
import { RolesColumns } from "src/app/modules/admin/administration/roles/roles.columns";
const routes: Routes = [
{
path: '',
title: 'Admin - Roles | Maxlan',
path: "",
title: "Admin - Roles | Maxlan",
component: RolesPage,
children: [
{
path: 'create',
path: "create",
component: RoleFormPageComponent,
canActivate: [PermissionGuard],
data: {
@ -24,7 +24,7 @@ const routes: Routes = [
},
},
{
path: 'edit/:id',
path: "edit/:id",
component: RoleFormPageComponent,
canActivate: [PermissionGuard],
data: {

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RolesPage } from 'src/app/modules/admin/administration/roles/roles.page';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service';
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RolesPage } from "src/app/modules/admin/administration/roles/roles.page";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { PageDataService } from "src/app/core/base/page.data.service";
import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
describe('RolesComponent', () => {
describe("RolesComponent", () => {
let component: RolesPage;
let fixture: ComponentFixture<RolesPage>;
@ -45,7 +45,7 @@ describe('RolesComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,21 +1,21 @@
import { Component } from '@angular/core';
import { PageBase } from 'src/app/core/base/page-base';
import { Role } from 'src/app/model/entities/role';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { RolesColumns } from 'src/app/modules/admin/administration/roles/roles.columns';
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service';
import { Component } from "@angular/core";
import { PageBase } from "src/app/core/base/page-base";
import { Role } from "src/app/model/entities/role";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { RolesColumns } from "src/app/modules/admin/administration/roles/roles.columns";
import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
@Component({
selector: 'app-roles',
templateUrl: './roles.page.html',
styleUrl: './roles.page.scss',
selector: "app-roles",
templateUrl: "./roles.page.html",
styleUrl: "./roles.page.scss",
})
export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
constructor(
private toast: ToastService,
private confirmation: ConfirmationDialogService
private confirmation: ConfirmationDialogService,
) {
super(true, {
read: [PermissionsEnum.roles],
@ -26,11 +26,11 @@ export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
});
}
load(silent?: boolean): void {
if (!silent) this.loading = true;
load(): void {
this.loading = true;
this.dataService
.load(this.filter, this.sort, this.skip, this.take)
.subscribe(result => {
.subscribe((result) => {
this.result = result;
this.loading = false;
});
@ -38,12 +38,12 @@ export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
delete(role: Role): void {
this.confirmation.confirmDialog({
header: 'dialog.delete.header',
message: 'dialog.delete.message',
header: "dialog.delete.header",
message: "dialog.delete.message",
accept: () => {
this.loading = true;
this.dataService.delete(role).subscribe(() => {
this.toast.success('action.deleted');
this.toast.success("action.deleted");
this.load();
});
},
@ -53,12 +53,12 @@ export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
restore(role: Role): void {
this.confirmation.confirmDialog({
header: 'dialog.restore.header',
message: 'dialog.restore.message',
header: "dialog.restore.header",
message: "dialog.restore.message",
accept: () => {
this.loading = true;
this.dataService.restore(role).subscribe(() => {
this.toast.success('action.restored');
this.toast.success("action.restored");
this.load();
});
},

View File

@ -1,105 +0,0 @@
import { Injectable, Provider } from '@angular/core';
import { Observable } from 'rxjs';
import { Apollo, gql } from 'apollo-angular';
import { catchError, map } from 'rxjs/operators';
import { SpinnerService } from 'src/app/service/spinner.service';
import { Setting } from 'src/app/model/entities/setting';
@Injectable()
export class SettingsDataService {
constructor(
private spinner: SpinnerService,
private apollo: Apollo
) {}
load(): Observable<Setting[]> {
return this.apollo
.query<{ settings: Setting[] }>({
query: gql`
query GetSettings {
settings {
key
value
}
}
`,
variables: {},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data.settings));
}
loadByKey(key: string): Observable<Setting> {
return this.apollo
.query<{ settings: Setting[] }>({
query: gql`
query getSetting($key: String!) {
settings(key: $key) {
key
value
}
}
`,
variables: {
key,
},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data.settings[0]));
}
onUpdate(): Observable<void> {
return this.apollo
.subscribe<{ settingChange: void }>({
query: gql`
subscription onSettingChange {
settingChange
}
`,
})
.pipe(map(result => result.data?.settingChange));
}
change(object: Setting): Observable<Setting | undefined> {
return this.apollo
.mutate<{ setting: { change: Setting } }>({
mutation: gql`
mutation changeSetting($input: SettingInput!) {
setting {
change(input: $input) {
key
value
}
}
}
`,
variables: {
input: {
key: object.key,
value: object.value,
},
},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data?.setting.change));
}
static provide(): Provider[] {
return [SettingsDataService];
}
}

View File

@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { SettingsPage } from 'src/app/modules/admin/administration/settings/settings.page';
import { SettingsDataService } from 'src/app/modules/admin/administration/settings/settings.data.service';
const routes: Routes = [
{
path: '',
title: 'Admin - settings | Maxlan',
component: SettingsPage,
},
];
@NgModule({
declarations: [SettingsPage],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
providers: [SettingsDataService.provide()],
})
export class SettingsModule {}

View File

@ -1,20 +0,0 @@
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-2" [formGroup]="settingsForm">
<div class="grid grid-cols-4 gap-1" *ngFor="let setting of settings">
<div><h2>{{ setting.key }}</h2></div>
<div>
<input type="text" pInputText [formControlName]="setting.key" />
</div>
</div>
</div>
<div class="flex items-start">
<p-button
label="{{ 'common.save' | translate }}"
class="btn"
icon="pi pi-save"
(onClick)="save()"
[disabled]="settingsForm.invalid || !hasChanges">
</p-button>
</div>
</div>

View File

@ -1,47 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsPage } from 'src/app/modules/admin/administration/settings/settings.page';
import { SettingsDataService } from 'src/app/modules/admin/administration/settings/settings.data.service';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
describe('SettingsComponent', () => {
let component: SettingsPage;
let fixture: ComponentFixture<SettingsPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SettingsPage],
imports: [SharedModule, TranslateModule.forRoot()],
providers: [
AuthService,
KeycloakService,
ErrorHandlingService,
ToastService,
MessageService,
ConfirmationService,
{
provide: ActivatedRoute,
useValue: {
snapshot: { params: of({}) },
},
},
SettingsDataService,
],
}).compileComponents();
fixture = TestBed.createComponent(SettingsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,94 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { SettingsDataService } from 'src/app/modules/admin/administration/settings/settings.data.service';
import { Setting } from 'src/app/model/entities/setting';
import { SpinnerService } from 'src/app/service/spinner.service';
import { catchError, takeUntil } from 'rxjs/operators';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { Subject } from 'rxjs';
@Component({
selector: 'app-settings',
templateUrl: './settings.page.html',
styleUrl: './settings.page.scss',
})
export class SettingsPage implements OnInit, OnDestroy {
settings: Setting[] = [];
settingsForm: FormGroup = this.fb.group({});
get hasChanges(): boolean {
return !this.settingsForm.pristine;
}
protected unsubscribe$ = new Subject<void>();
constructor(
private spinner: SpinnerService,
private data: SettingsDataService,
private fb: FormBuilder
) {}
ngOnInit() {
this.data
.onUpdate()
.pipe(takeUntil(this.unsubscribe$))
.subscribe(() => {
this.load();
});
this.spinner.show();
this.load();
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
load() {
this.settingsForm = this.fb.group({});
this.data
.load()
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.subscribe(settings => {
this.settings = settings;
this.settings.forEach(setting => {
this.settingsForm.addControl(
setting.key,
new FormControl(setting.value)
);
});
this.spinner.hide();
});
}
save() {
if (this.settingsForm.pristine) {
return;
}
const updatedSettings: Setting[] = this.settings
.filter(x => this.settingsForm.get(x.key)?.dirty)
.map(setting => ({
key: setting.key,
value: this.settingsForm.get(setting.key)?.value,
}));
for (const setting of updatedSettings) {
this.spinner.show();
this.data
.change(setting)
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.subscribe(() => this.spinner.hide());
}
}
}

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('UserFormpageComponent', () => {
describe("UserFormpageComponent", () => {
let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('UserFormpageComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -10,6 +10,7 @@ import {
} from 'src/app/model/auth/user';
import { Role } from 'src/app/model/entities/role';
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
import { CommonDataService } from 'src/app/modules/shared/service/common-data.service';
@Component({
selector: 'app-user-form-page',
@ -25,9 +26,12 @@ export class UserFormPageComponent extends FormPageBase<
notExistingUsers: NotExistingUser[] = [];
roles: Role[] = [];
constructor(private toast: ToastService) {
constructor(
private toast: ToastService,
private cds: CommonDataService
) {
super();
this.dataService.getAllRoles().subscribe(roles => {
this.cds.getAllRoles().subscribe(roles => {
this.roles = roles;
});
@ -107,7 +111,7 @@ export class UserFormPageComponent extends FormPageBase<
update(user: UserUpdateInput): void {
this.dataService.update(user).subscribe(() => {
this.spinner.hide();
this.toast.success('action.updated');
this.toast.success('action.created');
this.close();
});
}

View File

@ -1,11 +1,11 @@
import { Injectable, Provider } from '@angular/core';
import { Injectable, Provider } from "@angular/core";
import {
DB_MODEL_COLUMNS,
ID_COLUMN,
PageColumns,
} from 'src/app/core/base/page.columns';
import { TableColumn } from 'src/app/modules/shared/components/table/table.model';
import { User } from 'src/app/model/auth/user';
} from "src/app/core/base/page.columns";
import { TableColumn } from "src/app/modules/shared/components/table/table.model";
import { User } from "src/app/model/auth/user";
@Injectable()
export class UsersColumns extends PageColumns<User> {
@ -13,15 +13,15 @@ export class UsersColumns extends PageColumns<User> {
return [
ID_COLUMN,
{
name: 'username',
translationKey: 'user.username',
type: 'text',
name: "username",
label: "user.username",
type: "text",
value: (row: User) => row.username,
},
{
name: 'email',
translationKey: 'user.email',
type: 'text',
name: "email",
label: "user.email",
type: "text",
value: (row: User) => row.email,
},
...DB_MODEL_COLUMNS,

View File

@ -123,18 +123,6 @@ export class UsersDataService
.pipe(map(result => result.data.users.nodes[0]));
}
onChange(): Observable<void> {
return this.apollo
.subscribe<{ userChange: void }>({
query: gql`
subscription onUserChange {
userChange
}
`,
})
.pipe(map(result => result.data?.userChange));
}
create(object: UserCreateInput): Observable<User | undefined> {
return this.apollo
.mutate<{ user: { create: User } }>({
@ -251,29 +239,6 @@ export class UsersDataService
.pipe(map(result => result.data?.user.restore ?? false));
}
getAllRoles(): Observable<Role[]> {
return this.apollo
.query<{ roles: QueryResult<Role> }>({
query: gql`
query getRoles {
roles {
nodes {
id
name
}
}
}
`,
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data.roles.nodes));
}
getNotExistingUsersFromKeycloak(): Observable<NotExistingUser[]> {
return this.apollo
.query<{ notExistingUsersFromKeycloak: QueryResult<NotExistingUser> }>({

View File

@ -1,22 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { PermissionGuard } from 'src/app/core/guard/permission.guard';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { UserFormPageComponent } from 'src/app/modules/admin/administration/users/form-page/user-form-page.component';
import { UsersPage } from 'src/app/modules/admin/administration/users/users.page';
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
import { UsersColumns } from 'src/app/modules/admin/administration/users/users.columns';
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { UserFormPageComponent } from "src/app/modules/admin/administration/users/form-page/user-form-page.component";
import { UsersPage } from "src/app/modules/admin/administration/users/users.page";
import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
import { UsersColumns } from "src/app/modules/admin/administration/users/users.columns";
const routes: Routes = [
{
path: '',
title: 'Admin - Users | Maxlan',
path: "",
title: "Admin - Users | Maxlan",
component: UsersPage,
children: [
{
path: 'create',
path: "create",
component: UserFormPageComponent,
canActivate: [PermissionGuard],
data: {
@ -24,7 +24,7 @@ const routes: Routes = [
},
},
{
path: 'edit/:id',
path: "edit/:id",
component: UserFormPageComponent,
canActivate: [PermissionGuard],
data: {

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersPage } from 'src/app/modules/admin/administration/users/users.page';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { UsersPage } from "src/app/modules/admin/administration/users/users.page";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { PageDataService } from "src/app/core/base/page.data.service";
import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
describe('UsersComponent', () => {
describe("UsersComponent", () => {
let component: UsersPage;
let fixture: ComponentFixture<UsersPage>;
@ -45,7 +45,7 @@ describe('UsersComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,21 +1,21 @@
import { Component } from '@angular/core';
import { PageBase } from 'src/app/core/base/page-base';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { User } from 'src/app/model/auth/user';
import { UsersColumns } from 'src/app/modules/admin/administration/users/users.columns';
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
import { Component } from "@angular/core";
import { PageBase } from "src/app/core/base/page-base";
import { ToastService } from "src/app/service/toast.service";
import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { User } from "src/app/model/auth/user";
import { UsersColumns } from "src/app/modules/admin/administration/users/users.columns";
import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
@Component({
selector: 'app-users',
templateUrl: './users.page.html',
styleUrl: './users.page.scss',
selector: "app-users",
templateUrl: "./users.page.html",
styleUrl: "./users.page.scss",
})
export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
constructor(
private toast: ToastService,
private confirmation: ConfirmationDialogService
private confirmation: ConfirmationDialogService,
) {
super(true, {
read: [PermissionsEnum.users],
@ -26,11 +26,11 @@ export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
});
}
load(silent?: boolean): void {
if (!silent) this.loading = true;
load(): void {
this.loading = true;
this.dataService
.load(this.filter, this.sort, this.skip, this.take)
.subscribe(result => {
.subscribe((result) => {
this.result = result;
this.loading = false;
});
@ -38,12 +38,12 @@ export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
delete(user: User): void {
this.confirmation.confirmDialog({
header: 'dialog.delete.header',
message: 'dialog.delete.message',
header: "dialog.delete.header",
message: "dialog.delete.message",
accept: () => {
this.loading = true;
this.dataService.delete(user).subscribe(() => {
this.toast.success('action.deleted');
this.toast.success("action.deleted");
this.load();
});
},
@ -53,12 +53,12 @@ export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
restore(user: User): void {
this.confirmation.confirmDialog({
header: 'dialog.restore.header',
message: 'dialog.restore.message',
header: "dialog.restore.header",
message: "dialog.restore.message",
accept: () => {
this.loading = true;
this.dataService.restore(user).subscribe(() => {
this.toast.success('action.restored');
this.toast.success("action.restored");
this.load();
});
},

View File

@ -14,7 +14,7 @@ export class DomainsColumns extends PageColumns<Domain> {
ID_COLUMN,
{
name: 'name',
translationKey: 'common.name',
label: 'common.name',
type: 'text',
filterable: true,
value: (row: Domain) => row.name,

View File

@ -109,18 +109,6 @@ export class DomainsDataService
.pipe(map(result => result.data.domains.nodes[0]));
}
onChange(): Observable<void> {
return this.apollo
.subscribe<{ domainChange: void }>({
query: gql`
subscription onRoleChange {
domainChange
}
`,
})
.pipe(map(result => result.data?.domainChange));
}
create(object: DomainCreateInput): Observable<Domain | undefined> {
return this.apollo
.mutate<{ domain: { create: Domain } }>({

View File

@ -1,11 +1,11 @@
import { Injectable, Provider } from '@angular/core';
import { Injectable, Provider } from "@angular/core";
import {
DB_MODEL_COLUMNS,
ID_COLUMN,
PageColumns,
} from 'src/app/core/base/page.columns';
import { TableColumn } from 'src/app/modules/shared/components/table/table.model';
import { Group } from 'src/app/model/entities/group';
} from "src/app/core/base/page.columns";
import { TableColumn } from "src/app/modules/shared/components/table/table.model";
import { Group } from "src/app/model/entities/group";
@Injectable()
export class GroupsColumns extends PageColumns<Group> {
@ -13,9 +13,9 @@ export class GroupsColumns extends PageColumns<Group> {
return [
ID_COLUMN,
{
name: 'name',
translationKey: 'common.name',
type: 'text',
name: "name",
label: "common.name",
type: "text",
filterable: true,
value: (row: Group) => row.name,
},

View File

@ -117,18 +117,6 @@ export class GroupsDataService
.pipe(map(result => result.data.groups.nodes[0]));
}
onChange(): Observable<void> {
return this.apollo
.subscribe<{ groupChange: void }>({
query: gql`
subscription onRoleChange {
groupChange
}
`,
})
.pipe(map(result => result.data?.groupChange));
}
create(object: GroupCreateInput): Observable<Group | undefined> {
return this.apollo
.mutate<{ group: { create: Group } }>({

View File

@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule, Routes } from "@angular/router";
import { SharedModule } from "src/app/modules/shared/shared.module";
const routes: Routes = [];
@NgModule({
declarations: [],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
})
export class PublicModule {}

Some files were not shown because too many files have changed in this diff Show More