From b72916a8d90f57cfed821cca2ca3df226785ec4e Mon Sep 17 00:00:00 2001 From: edraft Date: Fri, 17 Jan 2025 15:54:14 +0100 Subject: [PATCH] Manage group/role assignment Closes #11 --- api/src/api/route_user_extension.py | 22 +-- api/src/api_graphql/abc/query_abc.py | 4 +- api/src/api_graphql/graphql/group.gql | 3 + .../api_graphql/input/group_create_input.py | 5 + .../api_graphql/input/group_update_input.py | 5 + api/src/api_graphql/mutation.py | 1 - .../api_graphql/mutations/group_mutation.py | 50 ++++++- api/src/api_graphql/queries/group_query.py | 17 ++- api/src/api_graphql/query.py | 20 ++- api/src/api_graphql/require_any_resolvers.py | 22 +++ api/src/api_graphql/service/query_context.py | 4 +- api/src/api_graphql/typing.py | 8 +- .../database/abc/data_access_object_abc.py | 86 +++++++----- api/src/core/get_value.py | 11 +- api/src/data/schemas/administration/user.py | 12 +- .../data/schemas/administration/user_dao.py | 21 ++- api/src/data/schemas/public/group_dao.py | 14 ++ .../schemas/public/group_role_assignment.py | 43 ++++++ .../public/group_role_assignment_dao.py | 37 +++++ ...2025-01-17-15-00-group-role-assignment.sql | 27 ++++ api/src/data/seeder/permission_seeder.py | 29 +++- api/src/redirector.py | 13 +- .../service/permission/permissions_enum.py | 3 + version.txt | 2 +- web/src/app/model/auth/permissionsEnum.ts | 1 + web/src/app/model/entities/group.ts | 4 + web/src/app/modules/admin/admin.module.ts | 7 +- .../form-page/role-form-page.component.html | 132 +++++++++--------- .../form-page/role-form-page.component.ts | 85 ++++++----- .../roles/roles.data.service.ts | 73 +++++----- .../form-page/user-form-page.component.ts | 68 ++++----- .../users/users.data.service.ts | 97 +++++-------- .../app/modules/admin/domains/domains.page.ts | 16 +-- .../form-page/group-form-page.component.html | 8 ++ .../form-page/group-form-page.component.ts | 63 ++++++--- .../admin/groups/groups.data.service.ts | 72 ++++++---- .../admin/short-urls/short-urls.page.html | 2 +- .../shared/service/common-data.service.ts | 40 ++++++ web/src/app/service/auth.service.ts | 41 +++--- web/src/app/service/error-handling.service.ts | 48 +++---- web/src/app/service/sidebar.service.ts | 1 + web/src/assets/i18n/de.json | 6 + web/src/assets/i18n/en.json | 6 + 43 files changed, 809 insertions(+), 420 deletions(-) create mode 100644 api/src/api_graphql/require_any_resolvers.py create mode 100644 api/src/data/schemas/public/group_role_assignment.py create mode 100644 api/src/data/schemas/public/group_role_assignment_dao.py create mode 100644 api/src/data/scripts/2025-01-17-15-00-group-role-assignment.sql create mode 100644 web/src/app/modules/shared/service/common-data.service.ts diff --git a/api/src/api/route_user_extension.py b/api/src/api/route_user_extension.py index 2240f9f..bcd1263 100644 --- a/api/src/api/route_user_extension.py +++ b/api/src/api/route_user_extension.py @@ -76,8 +76,8 @@ class RouteUserExtension: 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'])) + if "subGroups" in group and group["subGroups"]: + flat_list.extend(cls._flatten_groups(group["subGroups"])) return flat_list @classmethod @@ -88,9 +88,13 @@ class RouteUserExtension: 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() + 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) + 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) @@ -105,10 +109,12 @@ class RouteUserExtension: 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}, - ]) + await roleUserDao.get_single_by( + [ + {RoleUser.role_id: roles[role].id}, + {RoleUser.user_id: user.id}, + ] + ) for role in missing_roles ] ) diff --git a/api/src/api_graphql/abc/query_abc.py b/api/src/api_graphql/abc/query_abc.py index ecb7108..b19cee8 100644 --- a/api/src/api_graphql/abc/query_abc.py +++ b/api/src/api_graphql/abc/query_abc.py @@ -69,8 +69,10 @@ class QueryABC(ObjectType): ): 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 diff --git a/api/src/api_graphql/graphql/group.gql b/api/src/api_graphql/graphql/group.gql index 4a3880c..aaaf64c 100644 --- a/api/src/api_graphql/graphql/group.gql +++ b/api/src/api_graphql/graphql/group.gql @@ -9,6 +9,7 @@ type Group implements DbModel { name: String shortUrls: [ShortUrl] + roles: [Role] deleted: Boolean editor: User @@ -45,9 +46,11 @@ type GroupMutation { input GroupCreateInput { name: String! + roles: [ID] } input GroupUpdateInput { id: ID! name: String + roles: [ID] } \ No newline at end of file diff --git a/api/src/api_graphql/input/group_create_input.py b/api/src/api_graphql/input/group_create_input.py index 482bc64..994224e 100644 --- a/api/src/api_graphql/input/group_create_input.py +++ b/api/src/api_graphql/input/group_create_input.py @@ -7,7 +7,12 @@ class GroupCreateInput(InputABC): InputABC.__init__(self, src) self._name = self.option("name", str, required=True) + self._roles = self.option("roles", list[int]) @property def name(self) -> str: return self._name + + @property + def roles(self) -> list[int]: + return self._roles diff --git a/api/src/api_graphql/input/group_update_input.py b/api/src/api_graphql/input/group_update_input.py index a40d665..5d740cf 100644 --- a/api/src/api_graphql/input/group_update_input.py +++ b/api/src/api_graphql/input/group_update_input.py @@ -8,6 +8,7 @@ class GroupUpdateInput(InputABC): self._id = self.option("id", int, required=True) self._name = self.option("name", str) + self._roles = self.option("roles", list[int]) @property def id(self) -> int: @@ -16,3 +17,7 @@ class GroupUpdateInput(InputABC): @property def name(self) -> str: return self._name + + @property + def roles(self) -> list[int]: + return self._roles diff --git a/api/src/api_graphql/mutation.py b/api/src/api_graphql/mutation.py index 26df1b1..78a0f88 100644 --- a/api/src/api_graphql/mutation.py +++ b/api/src/api_graphql/mutation.py @@ -33,7 +33,6 @@ class Mutation(MutationABC): ], ) - self.add_mutation_type( "domain", "Domain", diff --git a/api/src/api_graphql/mutations/group_mutation.py b/api/src/api_graphql/mutations/group_mutation.py index c526366..4372cb1 100644 --- a/api/src/api_graphql/mutations/group_mutation.py +++ b/api/src/api_graphql/mutations/group_mutation.py @@ -1,9 +1,13 @@ +from typing import Optional + from api_graphql.abc.mutation_abc import MutationABC from api_graphql.input.group_create_input import GroupCreateInput from api_graphql.input.group_update_input import GroupUpdateInput from core.logger import APILogger from data.schemas.public.group import Group from data.schemas.public.group_dao import groupDao +from data.schemas.public.group_role_assignment import GroupRoleAssignment +from data.schemas.public.group_role_assignment_dao import groupRoleAssignmentDao from service.permission.permissions_enum import Permissions logger = APILogger(__name__) @@ -37,25 +41,61 @@ class GroupMutation(MutationABC): ) @staticmethod - async def resolve_create(obj: GroupCreateInput, *_): + async def _handle_group_role_assignments(gid: int, roles: Optional[list[int]]): + if roles is None: + return + + existing_roles = await groupDao.get_roles(gid) + existing_role_ids = {role.id for role in existing_roles} + + new_role_ids = set(roles) + + roles_to_add = new_role_ids - existing_role_ids + roles_to_remove = existing_role_ids - new_role_ids + + if roles_to_add: + group_role_assignments = [ + GroupRoleAssignment(0, gid, role_id) for role_id in roles_to_add + ] + await groupRoleAssignmentDao.create_many(group_role_assignments) + + if roles_to_remove: + assignments_to_remove = await groupRoleAssignmentDao.find_by( + [ + {GroupRoleAssignment.group_id: gid}, + {GroupRoleAssignment.role_id: {"in": roles_to_remove}}, + ] + ) + await groupRoleAssignmentDao.delete_many(assignments_to_remove) + + @classmethod + async def resolve_create(cls, obj: GroupCreateInput, *_): logger.debug(f"create group: {obj.__dict__}") group = Group( 0, obj.name, ) - nid = await groupDao.create(group) - return await groupDao.get_by_id(nid) + gid = await groupDao.create(group) - @staticmethod - async def resolve_update(obj: GroupUpdateInput, *_): + await cls._handle_group_role_assignments(gid, obj.roles) + + return await groupDao.get_by_id(gid) + + @classmethod + async def resolve_update(cls, obj: GroupUpdateInput, *_): logger.debug(f"update group: {input}") + if await groupDao.find_by_id(obj.id) is None: + raise ValueError(f"Group with id {obj.id} not found") + if obj.name is not None: group = await groupDao.get_by_id(obj.id) group.name = obj.name await groupDao.update(group) + await cls._handle_group_role_assignments(obj.id, obj.roles) + return await groupDao.get_by_id(obj.id) @staticmethod diff --git a/api/src/api_graphql/queries/group_query.py b/api/src/api_graphql/queries/group_query.py index 6917a2e..78b9c56 100644 --- a/api/src/api_graphql/queries/group_query.py +++ b/api/src/api_graphql/queries/group_query.py @@ -1,7 +1,11 @@ from api_graphql.abc.db_model_query_abc import DbModelQueryABC +from api_graphql.field.resolver_field_builder import ResolverFieldBuilder +from api_graphql.require_any_resolvers import group_by_assignment_resolver 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 service.permission.permissions_enum import Permissions class GroupQuery(DbModelQueryABC): @@ -9,8 +13,19 @@ class GroupQuery(DbModelQueryABC): DbModelQueryABC.__init__(self, "Group") self.set_field("name", lambda x, *_: x.name) - self.set_field("shortUrls", self._get_urls) + self.field( + ResolverFieldBuilder("shortUrls") + .with_resolver(self._get_urls) + .with_require_any([ + Permissions.groups, + ], [group_by_assignment_resolver]) + ) + self.set_field("roles", self._get_roles) @staticmethod async def _get_urls(group: Group, *_): return await shortUrlDao.find_by({ShortUrl.group_id: group.id}) + + @staticmethod + async def _get_roles(group: Group, *_): + return await groupDao.get_roles(group.id) diff --git a/api/src/api_graphql/query.py b/api/src/api_graphql/query.py index 43eeceb..09236f2 100644 --- a/api/src/api_graphql/query.py +++ b/api/src/api_graphql/query.py @@ -11,6 +11,7 @@ from api_graphql.filter.permission_filter import PermissionFilter from api_graphql.filter.role_filter import RoleFilter from api_graphql.filter.short_url_filter import ShortUrlFilter from api_graphql.filter.user_filter import UserFilter +from api_graphql.require_any_resolvers import group_by_assignment_resolver from data.schemas.administration.api_key import ApiKey from data.schemas.administration.api_key_dao import apiKeyDao from data.schemas.administration.user import User @@ -51,7 +52,15 @@ class Query(QueryABC): .with_dao(roleDao) .with_filter(RoleFilter) .with_sort(Sort[Role]) - .with_require_any_permission([Permissions.roles]) + .with_require_any_permission( + [ + Permissions.roles, + Permissions.users_create, + Permissions.users_update, + Permissions.groups_create, + Permissions.groups_update, + ] + ) ) self.field( @@ -83,7 +92,6 @@ class Query(QueryABC): .with_require_any_permission([Permissions.users_create]) ) - self.field( DaoFieldBuilder("domains") .with_dao(domainDao) @@ -102,21 +110,21 @@ class Query(QueryABC): .with_dao(groupDao) .with_filter(GroupFilter) .with_sort(Sort[Group]) - .with_require_any_permission( + .with_require_any( [ Permissions.groups, Permissions.short_urls_create, Permissions.short_urls_update, - ] + ], + [group_by_assignment_resolver] ) ) - # partially public to load redirect if not resolved/redirected by api self.field( DaoFieldBuilder("shortUrls") .with_dao(shortUrlDao) .with_filter(ShortUrlFilter) .with_sort(Sort[ShortUrl]) - .with_require_any_permission([Permissions.short_urls]) + .with_require_any([Permissions.short_urls], [group_by_assignment_resolver]) ) @staticmethod diff --git a/api/src/api_graphql/require_any_resolvers.py b/api/src/api_graphql/require_any_resolvers.py new file mode 100644 index 0000000..81a98e4 --- /dev/null +++ b/api/src/api_graphql/require_any_resolvers.py @@ -0,0 +1,22 @@ +from api_graphql.service.collection_result import CollectionResult +from api_graphql.service.query_context import QueryContext +from data.schemas.public.group_dao import groupDao +from service.permission.permissions_enum import Permissions + + +async def group_by_assignment_resolver(ctx: QueryContext) -> bool: + if not isinstance(ctx.data, CollectionResult): + return False + + if ctx.has_permission(Permissions.short_urls_by_assignment): + groups = [await x.group for x in ctx.data.nodes] + role_ids = {x.id for x in await ctx.user.roles} + filtered_groups = [ + g.id for g in groups if + g is not None and (roles := await groupDao.get_roles(g.id)) and all(r.id in role_ids for r in roles) + ] + + ctx.data.nodes = [node for node in ctx.data.nodes if (await node.group) is not None and (await node.group).id in filtered_groups] + return True + + return True diff --git a/api/src/api_graphql/service/query_context.py b/api/src/api_graphql/service/query_context.py index 8265fa6..40f85da 100644 --- a/api/src/api_graphql/service/query_context.py +++ b/api/src/api_graphql/service/query_context.py @@ -14,7 +14,7 @@ class QueryContext: self, data: Any, user: Optional[User], - user_permissions: Optional[list[Permission]], + user_permissions: Optional[list[Permissions]], *args, **kwargs ): @@ -23,7 +23,7 @@ class QueryContext: self._user = user if user_permissions is None: user_permissions = [] - self._user_permissions: list[str] = [x.name for x in user_permissions] + self._user_permissions: list[str] = [x.value for x in user_permissions] self._resolve_info = None for arg in args: diff --git a/api/src/api_graphql/typing.py b/api/src/api_graphql/typing.py index 612b34a..bfc685f 100644 --- a/api/src/api_graphql/typing.py +++ b/api/src/api_graphql/typing.py @@ -1,11 +1,15 @@ from collections.abc import Awaitable -from typing import Callable, Union, Optional +from typing import Callable, Union, Optional, Coroutine, Any from api_graphql.service.query_context import QueryContext from service.permission.permissions_enum import Permissions TRequireAnyPermissions = Optional[list[Permissions]] TRequireAnyResolvers = list[ - Union[Callable[[QueryContext], bool], Awaitable[[QueryContext], bool]] + Union[ + Callable[[QueryContext], bool], + Awaitable[[QueryContext], bool], + Callable[[QueryContext], Coroutine[Any, Any, bool]] + ] ] TRequireAny = tuple[TRequireAnyPermissions, TRequireAnyResolvers] diff --git a/api/src/core/database/abc/data_access_object_abc.py b/api/src/core/database/abc/data_access_object_abc.py index 938ac41..9325c5d 100644 --- a/api/src/core/database/abc/data_access_object_abc.py +++ b/api/src/core/database/abc/data_access_object_abc.py @@ -35,12 +35,12 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): return self._table_name def attribute( - self, - attr_name: Attribute, - attr_type: type, - db_name: str = None, - ignore=False, - primary_key=False, + self, + attr_name: Attribute, + attr_type: type, + db_name: str = None, + ignore=False, + primary_key=False, ): """ Add an attribute for db and object mapping to the data access object @@ -118,11 +118,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): return self.to_object(result[0]) async def get_by( - self, - filters: AttributeFilters = None, - sorts: AttributeSorts = None, - take: int = None, - skip: int = None, + self, + filters: AttributeFilters = None, + sorts: AttributeSorts = None, + take: int = None, + skip: int = None, ) -> list[T_DBM]: """ Get all objects by the given filters @@ -143,11 +143,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): return [self.to_object(x) for x in result] async def get_single_by( - self, - filters: AttributeFilters = None, - sorts: AttributeSorts = None, - take: int = None, - skip: int = None, + self, + filters: AttributeFilters = None, + sorts: AttributeSorts = None, + take: int = None, + skip: int = None, ) -> T_DBM: """ Get a single object by the given filters @@ -168,11 +168,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): return result[0] async def find_by( - self, - filters: AttributeFilters = None, - sorts: AttributeSorts = None, - take: int = None, - skip: int = None, + self, + filters: AttributeFilters = None, + sorts: AttributeSorts = None, + take: int = None, + skip: int = None, ) -> list[Optional[T_DBM]]: """ Find all objects by the given filters @@ -192,11 +192,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): return [self.to_object(x) for x in result] async def find_single_by( - self, - filters: AttributeFilters = None, - sorts: AttributeSorts = None, - take: int = None, - skip: int = None, + self, + filters: AttributeFilters = None, + sorts: AttributeSorts = None, + take: int = None, + skip: int = None, ) -> Optional[T_DBM]: """ Find a single object by the given filters @@ -296,7 +296,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): await self._db.execute(query) async def _build_delete_statement( - self, obj: T_DBM, hard_delete: bool = False + self, obj: T_DBM, hard_delete: bool = False ) -> str: if hard_delete: return f""" @@ -370,6 +370,9 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): if isinstance(value, NoneType): return "NULL" + if value is None: + return "NULL" + if isinstance(value, Enum): return str(value.value) @@ -403,21 +406,21 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): return cast_type(value) def _build_conditional_query( - self, - filters: AttributeFilters = None, - sorts: AttributeSorts = None, - take: int = None, - skip: int = None, + self, + filters: AttributeFilters = None, + sorts: AttributeSorts = None, + take: int = None, + skip: int = None, ) -> str: query = f"SELECT * FROM {self._table_name}" - if filters and len(filters) > 0: + if filters is not None and (not isinstance(filters, list) or len(filters) > 0): query += f" WHERE {self._build_conditions(filters)}" - if sorts and len(sorts) > 0: + if sorts is not None and (not isinstance(sorts, list) or len(sorts) > 0): query += f" ORDER BY {self._build_order_by(sorts)}" - if take: + if take is not None: query += f" LIMIT {take}" - if skip: + if skip is not None: query += f" OFFSET {skip}" return query @@ -452,14 +455,21 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): ) else: sub_conditions.append( - f"{db_name} = {self._get_value_sql(value)}" + self._get_value_validation_sql(db_name, value) ) conditions.append(f"({' OR '.join(sub_conditions)})") else: - conditions.append(f"{db_name} = {self._get_value_sql(values)}") + conditions.append(self._get_value_validation_sql(db_name, values)) return " AND ".join(conditions) + def _get_value_validation_sql(self, field: str, value: Any): + value = self._get_value_sql(value) + + if value == "NULL": + return f"{field} IS NULL" + return f"{field} = {value}" + def _build_condition(self, db_name: str, operator: str, value: Any) -> str: """ Build individual SQL condition based on the operator diff --git a/api/src/core/get_value.py b/api/src/core/get_value.py index 89974ae..aff4788 100644 --- a/api/src/core/get_value.py +++ b/api/src/core/get_value.py @@ -24,14 +24,21 @@ def get_value( if key in source: value = source[key] - if isinstance(value, cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__): + if isinstance( + value, + cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__, + ): return value try: if cast_type == bool: return value.lower() in ["true", "1"] - if (cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__) == list: + if ( + cast_type + if not hasattr(cast_type, "__origin__") + else cast_type.__origin__ + ) == list: subtype = ( cast_type.__args__[0] if hasattr(cast_type, "__args__") else None ) diff --git a/api/src/data/schemas/administration/user.py b/api/src/data/schemas/administration/user.py index da5438b..3a44626 100644 --- a/api/src/data/schemas/administration/user.py +++ b/api/src/data/schemas/administration/user.py @@ -42,17 +42,9 @@ class User(DbModelABC): @async_property async def permissions(self): - from data.schemas.permission.role_user_dao import roleUserDao - from data.schemas.permission.role_permission_dao import rolePermissionDao - from data.schemas.permission.permission_dao import permissionDao + from data.schemas.administration.user_dao import userDao - x = [ - rp.permission_id - for x in await roleUserDao.get_by_user_id(self.id) - for rp in await rolePermissionDao.get_by_role_id(x.role_id) - ] - - return await permissionDao.get_by({"id": {"in": x}}) + return await userDao.get_permissions(self.id) async def has_permission(self, permission: Permissions) -> bool: from data.schemas.administration.user_dao import userDao diff --git a/api/src/data/schemas/administration/user_dao.py b/api/src/data/schemas/administration/user_dao.py index feaba98..7759dd3 100644 --- a/api/src/data/schemas/administration/user_dao.py +++ b/api/src/data/schemas/administration/user_dao.py @@ -33,13 +33,30 @@ class UserDao(DbModelDaoABC[User]): SELECT COUNT(*) FROM permission.role_users ru JOIN permission.role_permissions rp ON ru.roleId = rp.roleId - WHERE ru.userId = {user_id} AND rp.permissionId = {p.id}; + WHERE ru.userId = {user_id} + AND rp.permissionId = {p.id} + AND ru.deleted = FALSE + AND rp.deleted = FALSE; """ ) if result is None or len(result) == 0: return False - return True + return result[0]["count"] > 0 + + async def get_permissions(self, user_id: int) -> list[Permissions]: + result = await self._db.select_map( + f""" + SELECT p.* + FROM permission.permissions p + JOIN permission.role_permissions rp ON p.id = rp.permissionId + JOIN permission.role_users ru ON rp.roleId = ru.roleId + WHERE ru.userId = {user_id} + AND rp.deleted = FALSE + AND ru.deleted = FALSE; + """ + ) + return [Permissions(p["name"]) for p in result] userDao = UserDao() diff --git a/api/src/data/schemas/public/group_dao.py b/api/src/data/schemas/public/group_dao.py index cc19ec3..52603f8 100644 --- a/api/src/data/schemas/public/group_dao.py +++ b/api/src/data/schemas/public/group_dao.py @@ -17,5 +17,19 @@ class GroupDao(DbModelDaoABC[Group]): ) return self.to_object(result[0]) + async def get_roles(self, group_id: int): + result = await self._db.select_map( + f""" + SELECT r.* + FROM permission.roles r + JOIN public.group_role_assignments gra ON r.id = gra.roleId + WHERE gra.groupId = {group_id} + AND gra.deleted = FALSE + """ + ) + from data.schemas.permission.role_dao import roleDao + + return [roleDao.to_object(x) for x in result] + groupDao = GroupDao() diff --git a/api/src/data/schemas/public/group_role_assignment.py b/api/src/data/schemas/public/group_role_assignment.py new file mode 100644 index 0000000..cc4618d --- /dev/null +++ b/api/src/data/schemas/public/group_role_assignment.py @@ -0,0 +1,43 @@ +from datetime import datetime +from typing import Optional + +from async_property import async_property + +from core.database.abc.db_model_abc import DbModelABC +from core.typing import SerialId + + +class GroupRoleAssignment(DbModelABC): + def __init__( + self, + id: SerialId, + group_id: SerialId, + role_id: SerialId, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._group_id = group_id + self._role_id = role_id + + @property + def group_id(self) -> SerialId: + return self._group_id + + @async_property + async def group(self): + from data.schemas.public.group_dao import groupDao + + return await groupDao.get_by_id(self._group_id) + + @property + def role_id(self) -> SerialId: + return self._role_id + + @async_property + async def role(self): + from data.schemas.permission.role_dao import roleDao + + return await roleDao.get_by_id(self._role_id) diff --git a/api/src/data/schemas/public/group_role_assignment_dao.py b/api/src/data/schemas/public/group_role_assignment_dao.py new file mode 100644 index 0000000..4f2efc7 --- /dev/null +++ b/api/src/data/schemas/public/group_role_assignment_dao.py @@ -0,0 +1,37 @@ +from core.logger import DBLogger +from data.schemas.public.group_role_assignment import GroupRoleAssignment + +logger = DBLogger(__name__) + +from core.database.abc.db_model_dao_abc import DbModelDaoABC + + +class GroupRoleAssignmentDao(DbModelDaoABC[GroupRoleAssignment]): + def __init__(self): + DbModelDaoABC.__init__( + self, __name__, GroupRoleAssignment, "public.group_role_assignments" + ) + + self.attribute(GroupRoleAssignment.group_id, int) + self.attribute(GroupRoleAssignment.role_id, int) + + async def get_by_group_id( + self, gid: int, with_deleted=False + ) -> list[GroupRoleAssignment]: + f = [{GroupRoleAssignment.group_id: gid}] + if not with_deleted: + f.append({GroupRoleAssignment.deleted: False}) + + return await self.find_by(f) + + async def get_by_role_id( + self, rid: int, with_deleted=False + ) -> list[GroupRoleAssignment]: + f = [{GroupRoleAssignment.role_id: rid}] + if not with_deleted: + f.append({GroupRoleAssignment.deleted: False}) + + return await self.find_by(f) + + +groupRoleAssignmentDao = GroupRoleAssignmentDao() diff --git a/api/src/data/scripts/2025-01-17-15-00-group-role-assignment.sql b/api/src/data/scripts/2025-01-17-15-00-group-role-assignment.sql new file mode 100644 index 0000000..66a32a3 --- /dev/null +++ b/api/src/data/scripts/2025-01-17-15-00-group-role-assignment.sql @@ -0,0 +1,27 @@ +CREATE + SCHEMA IF NOT EXISTS public; + +-- groups +CREATE TABLE IF NOT EXISTS public.group_role_assignments +( + Id SERIAL PRIMARY KEY, + GroupId INT NOT NULL REFERENCES public.groups (Id), + RoleId INT NOT NULL REFERENCES permission.roles (Id), + -- for history + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + EditorId INT NULL REFERENCES administration.users (Id), + CreatedUtc timestamptz NOT NULL DEFAULT NOW(), + UpdatedUtc timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS public.group_role_assignments_history +( + LIKE public.group_role_assignments +); + +CREATE TRIGGER group_role_assignment_history_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON public.group_role_assignments + FOR EACH ROW +EXECUTE FUNCTION public.history_trigger_function(); + diff --git a/api/src/data/seeder/permission_seeder.py b/api/src/data/seeder/permission_seeder.py index bf85cf4..9fabe4f 100644 --- a/api/src/data/seeder/permission_seeder.py +++ b/api/src/data/seeder/permission_seeder.py @@ -25,7 +25,8 @@ class PermissionSeeder(DataSeederABC): possible_permissions = [permission.value for permission in Permissions] if len(permissions) == len(possible_permissions): - logger.info("Permissions already completed") + logger.info("Permissions already existing") + await self._update_missing_descriptions() return logger.warning("Permissions incomplete") @@ -41,6 +42,7 @@ class PermissionSeeder(DataSeederABC): if permission not in permission_names ] ) + await self._update_missing_descriptions() await self._add_missing_to_role() await self._add_missing_to_api_key() @@ -78,3 +80,28 @@ class PermissionSeeder(DataSeederABC): if permission.id not in [x.permission_id for x in admin_permissions] ] await apiKeyPermissionDao.create_many(to_assign) + + @staticmethod + async def _update_missing_descriptions(): + permissions = { + permission.name: permission + for permission in await permissionDao.find_by( + [{Permission.description: None}] + ) + } + to_update = [] + + if len(permissions) == 0: + return + + for key in PERMISSION_DESCRIPTIONS: + if key.value not in permissions: + continue + + permissions[key.value].description = PERMISSION_DESCRIPTIONS[key] + to_update.append(permissions[key.value]) + + if len(to_update) == 0: + return + + await permissionDao.update_many(to_update) diff --git a/api/src/redirector.py b/api/src/redirector.py index dbe045f..153967f 100644 --- a/api/src/redirector.py +++ b/api/src/redirector.py @@ -24,10 +24,12 @@ class Redirector(Flask): app = Redirector(__name__) + @app.route("/") def index(): return render_template("404.html"), 404 + @app.route("/") async def _handle_request(path: str): short_url = await _find_short_url_by_url(path) @@ -36,14 +38,19 @@ async def _handle_request(path: str): domains = Environment.get("DOMAINS", list[str], []) domain = await short_url.domain - logger.debug(f"Domain: {domain.name if domain is not None else None}, request.host: {request.host}") + logger.debug( + f"Domain: {domain.name if domain is not None else None}, request.host: {request.host}" + ) host = request.host if ":" in host: host = host.split(":")[0] domain_strict_mode = Environment.get("DOMAIN_STRICT_MODE", bool, False) - if domain is not None and (domain.name not in domains or (domain_strict_mode and not host.endswith(domain.name))): + if domain is not None and ( + domain.name not in domains + or (domain_strict_mode and not host.endswith(domain.name)) + ): return render_template("404.html"), 404 user_agent = request.headers.get("User-Agent", "").lower() @@ -71,6 +78,7 @@ async def _handle_short_url(path: str, short_url: ShortUrl): return _do_redirect(short_url.target_url) + async def _track_visit(short_url: ShortUrl): try: await shortUrlVisitDao.create( @@ -79,6 +87,7 @@ async def _track_visit(short_url: ShortUrl): except Exception as e: logger.error(f"Failed to update short url {short_url.short_url} with error", e) + async def _find_short_url_by_url(url: str) -> ShortUrl: return await shortUrlDao.find_single_by({ShortUrl.short_url: url}) diff --git a/api/src/service/permission/permissions_enum.py b/api/src/service/permission/permissions_enum.py index 1fd1abc..2ccdaa2 100644 --- a/api/src/service/permission/permissions_enum.py +++ b/api/src/service/permission/permissions_enum.py @@ -45,6 +45,7 @@ class Permissions(Enum): # short_urls short_urls = "short_urls" + short_urls_by_assignment = "short_urls.by_assignment" short_urls_create = "short_urls.create" short_urls_update = "short_urls.update" short_urls_delete = "short_urls.delete" @@ -52,4 +53,6 @@ class Permissions(Enum): PERMISSION_DESCRIPTIONS = { Permissions.users_update: "Edit users, including changing their roles", + Permissions.short_urls: "See all URLs", + Permissions.short_urls_by_assignment: "See all short urls assigned to a group by role", } diff --git a/version.txt b/version.txt index 867e524..cb174d5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.2.0 \ No newline at end of file +1.2.1 \ No newline at end of file diff --git a/web/src/app/model/auth/permissionsEnum.ts b/web/src/app/model/auth/permissionsEnum.ts index b063db6..5885651 100644 --- a/web/src/app/model/auth/permissionsEnum.ts +++ b/web/src/app/model/auth/permissionsEnum.ts @@ -29,6 +29,7 @@ export enum PermissionsEnum { groupsDelete = 'groups.delete', shortUrls = 'short_urls', + shortUrlsByAssignment = 'short_urls.by_assignment', shortUrlsCreate = 'short_urls.create', shortUrlsUpdate = 'short_urls.update', shortUrlsDelete = 'short_urls.delete', diff --git a/web/src/app/model/entities/group.ts b/web/src/app/model/entities/group.ts index 5437ff1..94be2dc 100644 --- a/web/src/app/model/entities/group.ts +++ b/web/src/app/model/entities/group.ts @@ -1,14 +1,18 @@ import { DbModel } from 'src/app/model/entities/db-model'; +import { Role } from 'src/app/model/entities/role'; export interface Group extends DbModel { name: string; + roles: Role[]; } export interface GroupCreateInput { name: string; + roles: Role[]; } export interface GroupUpdateInput { id: number; name: string; + roles: Role[]; } diff --git a/web/src/app/modules/admin/admin.module.ts b/web/src/app/modules/admin/admin.module.ts index 57af25b..d47a27d 100644 --- a/web/src/app/modules/admin/admin.module.ts +++ b/web/src/app/modules/admin/admin.module.ts @@ -31,7 +31,12 @@ const routes: Routes = [ m => m.ShortUrlsModule ), canActivate: [PermissionGuard], - data: { permissions: [PermissionsEnum.shortUrls] }, + data: { + permissions: [ + PermissionsEnum.shortUrls, + PermissionsEnum.shortUrlsByAssignment, + ], + }, }, { path: 'administration', diff --git a/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.html b/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.html index 09654f7..a1ff83f 100644 --- a/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.html +++ b/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.html @@ -1,67 +1,73 @@ - -

- {{ 'common.role' | translate }} - {{ - (isUpdate ? 'sidebar.header.update' : 'sidebar.header.create') - | translate - }} -

-
+ *ngIf="node" + [formGroup]="form" + [isUpdate]="isUpdate" + (onSave)="save()" + (onClose)="close()"> + +

+ {{ 'common.role' | translate }} + {{ + (isUpdate ? 'sidebar.header.update' : 'sidebar.header.create') + | translate + }} +

+
- -
-

{{ 'common.id' | translate }}

- -
-
-

{{ 'common.name' | translate }}

- -
-
-

{{ 'common.description' | translate }}

- -
-
- -
-
-
-
- -
- -
-
- -
- - - > - -
+ +
+

{{ 'common.id' | translate }}

+
-
-
- +
+

{{ 'common.name' | translate }}

+ +
+
+

{{ 'common.description' | translate }}

+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+ + + +
+
+

+ {{ permission.description }} +

+
+
+
+
+
+ diff --git a/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.ts b/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.ts index 4d950f4..2cbd74f 100644 --- a/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.ts +++ b/web/src/app/modules/admin/administration/roles/form-page/role-form-page.component.ts @@ -1,21 +1,22 @@ -import { Component } from "@angular/core"; +import { Component } from '@angular/core'; import { Permission, Role, RoleCreateInput, RoleUpdateInput, -} from "src/app/model/entities/role"; -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 { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; +} from 'src/app/model/entities/role'; +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 { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: "app-role-form-page", - templateUrl: "./role-form-page.component.html", - styleUrl: "./role-form-page.component.scss", + selector: 'app-role-form-page', + templateUrl: './role-form-page.component.html', + styleUrl: './role-form-page.component.scss', }) export class RoleFormPageComponent extends FormPageBase< Role, @@ -26,7 +27,10 @@ export class RoleFormPageComponent extends FormPageBase< permissionGroups: { [key: string]: Permission[] } = {}; allPermissions: Permission[] = []; - constructor(private toast: ToastService) { + constructor( + private toast: ToastService, + private translate: TranslateService + ) { super(); this.initializePermissions().then(() => { if (!this.nodeId) { @@ -36,7 +40,7 @@ export class RoleFormPageComponent extends FormPageBase< return; } - this.dataService.loadById(this.nodeId).subscribe((role) => { + this.dataService.loadById(this.nodeId).subscribe(role => { this.node = role; this.setForm(this.node); }); @@ -45,12 +49,17 @@ export class RoleFormPageComponent extends FormPageBase< async initializePermissions() { const permissions = await firstValueFrom( - this.dataService.getAllPermissions(), + this.dataService.getAllPermissions() ); - this.allPermissions = permissions; + this.allPermissions = permissions.map(x => { + const key = `permission_descriptions.${x.name}`; + const description = this.translate.instant(key); + x.description = description === key ? undefined : description; + return x; + }); 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] = []; @@ -58,10 +67,10 @@ export class RoleFormPageComponent 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(false)); }); } @@ -76,48 +85,48 @@ export class RoleFormPageComponent extends FormPageBase< name: new FormControl(undefined, Validators.required), description: new FormControl(undefined), }); - this.form.controls["id"].disable(); + this.form.controls['id'].disable(); } setForm(node?: Role) { - this.form.controls["id"].setValue(node?.id); - this.form.controls["name"].setValue(node?.name); - this.form.controls["description"].setValue(node?.description); + this.form.controls['id'].setValue(node?.id); + this.form.controls['name'].setValue(node?.name); + this.form.controls['description'].setValue(node?.description); if (!node) return; const permissions = node.permissions ?? []; - permissions.forEach((p) => { + permissions.forEach(p => { this.form.controls[p.name].setValue(true); }); } getCreateInput(): RoleCreateInput { return { - name: this.form.controls["name"].pristine + name: this.form.controls['name'].pristine ? undefined - : (this.form.controls["name"].value ?? undefined), - description: this.form.controls["description"].pristine + : (this.form.controls['name'].value ?? undefined), + description: this.form.controls['description'].pristine ? undefined - : (this.form.controls["description"].value ?? undefined), + : (this.form.controls['description'].value ?? undefined), permissions: this.allPermissions.filter( - (p) => this.form.controls[p.name].value, + p => this.form.controls[p.name].value ), }; } getUpdateInput(): RoleUpdateInput { return { - id: this.form.controls["id"].value, - name: this.form.controls["name"].pristine + id: this.form.controls['id'].value, + name: this.form.controls['name'].pristine ? undefined - : this.form.controls["name"].value, - description: this.form.controls["description"].pristine + : this.form.controls['name'].value, + description: this.form.controls['description'].pristine ? undefined - : this.form.controls["description"].value, + : this.form.controls['description'].value, permissions: this.allPermissions.filter( - (p) => this.form.controls[p.name].value, + p => this.form.controls[p.name].value ), }; } @@ -125,7 +134,7 @@ export class RoleFormPageComponent extends FormPageBase< create(role: RoleCreateInput): void { this.dataService.create(role).subscribe(() => { this.spinner.hide(); - this.toast.success("action.created"); + this.toast.success('action.created'); this.close(); }); } @@ -133,20 +142,20 @@ export class RoleFormPageComponent extends FormPageBase< update(role: RoleUpdateInput): void { this.dataService.update(role).subscribe(() => { this.spinner.hide(); - this.toast.success("action.created"); + 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 ); } diff --git a/web/src/app/modules/admin/administration/roles/roles.data.service.ts b/web/src/app/modules/admin/administration/roles/roles.data.service.ts index 909d969..a2632f1 100644 --- a/web/src/app/modules/admin/administration/roles/roles.data.service.ts +++ b/web/src/app/modules/admin/administration/roles/roles.data.service.ts @@ -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"; +} from 'src/app/core/base/page.data.service'; import { Permission, Role, RoleCreateInput, RoleUpdateInput, -} 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/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'; @Injectable() export class RolesDataService @@ -32,7 +32,7 @@ export class RolesDataService { constructor( private spinner: SpinnerService, - private apollo: Apollo, + private apollo: Apollo ) { super(); } @@ -41,7 +41,7 @@ export class RolesDataService filter?: Filter[] | undefined, sort?: Sort[] | undefined, skip?: number | undefined, - take?: number | undefined, + take?: number | undefined ): Observable> { return this.apollo .query<{ roles: QueryResult }>({ @@ -75,12 +75,12 @@ export class RolesDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data.roles)); + .pipe(map(result => result.data.roles)); } loadById(id: number): Observable { @@ -112,12 +112,12 @@ export class RolesDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data.roles.nodes[0])); + .pipe(map(result => result.data.roles.nodes[0])); } create(object: RoleCreateInput): Observable { @@ -145,17 +145,17 @@ export class RolesDataService input: { name: object.name, description: object.description, - 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?.role.create)); + .pipe(map(result => result.data?.role.create)); } update(object: RoleUpdateInput): Observable { @@ -184,17 +184,17 @@ export class RolesDataService id: object.id, name: object.name, description: object.description, - 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?.role.update)); + .pipe(map(result => result.data?.role.update)); } delete(object: Role): Observable { @@ -212,12 +212,12 @@ export class RolesDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.role.delete ?? false)); + .pipe(map(result => result.data?.role.delete ?? false)); } restore(object: Role): Observable { @@ -235,12 +235,12 @@ export class RolesDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.role.restore ?? false)); + .pipe(map(result => result.data?.role.restore ?? false)); } getAllPermissions(): Observable { @@ -252,18 +252,19 @@ export class RolesDataService nodes { id name + description } } } `, }) .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[] { diff --git a/web/src/app/modules/admin/administration/users/form-page/user-form-page.component.ts b/web/src/app/modules/admin/administration/users/form-page/user-form-page.component.ts index ba8a1b2..00bd9a8 100644 --- a/web/src/app/modules/admin/administration/users/form-page/user-form-page.component.ts +++ b/web/src/app/modules/admin/administration/users/form-page/user-form-page.component.ts @@ -1,20 +1,21 @@ -import { Component } from "@angular/core"; -import { FormControl, FormGroup } from "@angular/forms"; -import { ToastService } from "src/app/service/toast.service"; -import { FormPageBase } from "src/app/core/base/form-page-base"; +import { Component } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { ToastService } from 'src/app/service/toast.service'; +import { FormPageBase } from 'src/app/core/base/form-page-base'; import { NotExistingUser, User, UserCreateInput, UserUpdateInput, -} 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"; +} 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", - templateUrl: "./user-form-page.component.html", - styleUrl: "./user-form-page.component.scss", + selector: 'app-user-form-page', + templateUrl: './user-form-page.component.html', + styleUrl: './user-form-page.component.scss', }) export class UserFormPageComponent extends FormPageBase< User, @@ -25,14 +26,17 @@ 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; }); if (!this.nodeId) { - this.dataService.getNotExistingUsersFromKeycloak().subscribe((users) => { + this.dataService.getNotExistingUsersFromKeycloak().subscribe(users => { this.notExistingUsers = users; this.node = this.new(); this.setForm(this.node); @@ -41,7 +45,7 @@ export class UserFormPageComponent extends FormPageBase< return; } - this.dataService.loadById(this.nodeId).subscribe((user) => { + this.dataService.loadById(this.nodeId).subscribe(user => { this.node = user; this.setForm(this.node); }); @@ -59,47 +63,47 @@ export class UserFormPageComponent extends FormPageBase< email: new FormControl(undefined), roles: new FormControl([]), }); - this.form.controls["id"].disable(); - this.form.controls["username"].disable(); - this.form.controls["email"].disable(); + this.form.controls['id'].disable(); + this.form.controls['username'].disable(); + this.form.controls['email'].disable(); } setForm(node?: User) { - this.form.controls["id"].setValue(node?.id); - this.form.controls["username"].setValue(node?.username); - this.form.controls["email"].setValue(node?.email); - this.form.controls["roles"].setValue(node?.roles ?? []); + this.form.controls['id'].setValue(node?.id); + this.form.controls['username'].setValue(node?.username); + this.form.controls['email'].setValue(node?.email); + this.form.controls['roles'].setValue(node?.roles ?? []); if (this.notExistingUsers.length > 0) { - this.form.controls["id"].enable(); - this.form.controls["keycloakId"].reset(undefined, { required: true }); + this.form.controls['id'].enable(); + this.form.controls['keycloakId'].reset(undefined, { required: true }); } } getCreateInput(): UserCreateInput { return { - keycloakId: this.form.controls["keycloakId"].pristine + keycloakId: this.form.controls['keycloakId'].pristine ? undefined - : this.form.controls["keycloakId"].value, - roles: this.form.controls["roles"].pristine + : this.form.controls['keycloakId'].value, + roles: this.form.controls['roles'].pristine ? undefined - : this.form.controls["roles"].value, + : this.form.controls['roles'].value, }; } getUpdateInput(): UserUpdateInput { return { - id: this.form.controls["id"].value, - roles: this.form.controls["roles"].pristine + id: this.form.controls['id'].value, + roles: this.form.controls['roles'].pristine ? undefined - : this.form.controls["roles"].value, + : this.form.controls['roles'].value, }; } create(user: UserCreateInput): void { this.dataService.create(user).subscribe(() => { this.spinner.hide(); - this.toast.success("action.created"); + this.toast.success('action.created'); this.close(); }); } @@ -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.created"); + this.toast.success('action.created'); this.close(); }); } diff --git a/web/src/app/modules/admin/administration/users/users.data.service.ts b/web/src/app/modules/admin/administration/users/users.data.service.ts index f11005d..1a2eb58 100644 --- a/web/src/app/modules/admin/administration/users/users.data.service.ts +++ b/web/src/app/modules/admin/administration/users/users.data.service.ts @@ -1,26 +1,26 @@ -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 { 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 { 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 { NotExistingUser, User, UserCreateInput, UserUpdateInput, -} from "src/app/model/auth/user"; -import { Role } from "src/app/model/entities/role"; +} from 'src/app/model/auth/user'; +import { Role } from 'src/app/model/entities/role'; @Injectable() export class UsersDataService @@ -33,7 +33,7 @@ export class UsersDataService { constructor( private spinner: SpinnerService, - private apollo: Apollo, + private apollo: Apollo ) { super(); } @@ -42,7 +42,7 @@ export class UsersDataService filter?: Filter[] | undefined, sort?: Sort[] | undefined, skip?: number | undefined, - take?: number | undefined, + take?: number | undefined ): Observable> { return this.apollo .query<{ users: QueryResult }>({ @@ -77,12 +77,12 @@ export class UsersDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data.users)); + .pipe(map(result => result.data.users)); } loadById(id: number): Observable { @@ -115,12 +115,12 @@ export class UsersDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data.users.nodes[0])); + .pipe(map(result => result.data.users.nodes[0])); } create(object: UserCreateInput): Observable { @@ -145,17 +145,17 @@ export class UsersDataService variables: { input: { keycloakId: object.keycloakId, - roles: object.roles?.map((x) => x.id), + roles: object.roles?.map(x => x.id), }, }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.user.create)); + .pipe(map(result => result.data?.user.create)); } update(object: UserUpdateInput): Observable { @@ -180,17 +180,17 @@ export class UsersDataService variables: { input: { id: object.id, - roles: object.roles?.map((x) => x.id), + roles: object.roles?.map(x => x.id), }, }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.user.update)); + .pipe(map(result => result.data?.user.update)); } delete(object: User): Observable { @@ -208,12 +208,12 @@ export class UsersDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.user.delete ?? false)); + .pipe(map(result => result.data?.user.delete ?? false)); } restore(object: User): Observable { @@ -231,35 +231,12 @@ export class UsersDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.user.restore ?? false)); - } - - getAllRoles(): Observable { - return this.apollo - .query<{ roles: QueryResult }>({ - query: gql` - query getRoles { - roles { - nodes { - id - name - } - } - } - `, - }) - .pipe( - catchError((err) => { - this.spinner.hide(); - throw err; - }), - ) - .pipe(map((result) => result.data.roles.nodes)); + .pipe(map(result => result.data?.user.restore ?? false)); } getNotExistingUsersFromKeycloak(): Observable { @@ -277,12 +254,12 @@ export class UsersDataService `, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data.notExistingUsersFromKeycloak.nodes)); + .pipe(map(result => result.data.notExistingUsersFromKeycloak.nodes)); } static provide(): Provider[] { diff --git a/web/src/app/modules/admin/domains/domains.page.ts b/web/src/app/modules/admin/domains/domains.page.ts index bc10a7f..14ba9e3 100644 --- a/web/src/app/modules/admin/domains/domains.page.ts +++ b/web/src/app/modules/admin/domains/domains.page.ts @@ -3,9 +3,9 @@ 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 { Group } from 'src/app/model/entities/group'; import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service'; import { DomainsColumns } from 'src/app/modules/admin/domains/domains.columns'; +import { Domain } from 'src/app/model/entities/domain'; @Component({ selector: 'app-domains', @@ -13,7 +13,7 @@ import { DomainsColumns } from 'src/app/modules/admin/domains/domains.columns'; styleUrl: './domains.page.scss', }) export class DomainsPage extends PageBase< - Group, + Domain, DomainsDataService, DomainsColumns > { @@ -40,33 +40,33 @@ export class DomainsPage extends PageBase< }); } - delete(group: Group): void { + delete(domain: Domain): void { this.confirmation.confirmDialog({ header: 'dialog.delete.header', message: 'dialog.delete.message', accept: () => { this.loading = true; - this.dataService.delete(group).subscribe(() => { + this.dataService.delete(domain).subscribe(() => { this.toast.success('action.deleted'); this.load(); }); }, - messageParams: { entity: group.name }, + messageParams: { entity: domain.name }, }); } - restore(group: Group): void { + restore(domain: Domain): void { this.confirmation.confirmDialog({ header: 'dialog.restore.header', message: 'dialog.restore.message', accept: () => { this.loading = true; - this.dataService.restore(group).subscribe(() => { + this.dataService.restore(domain).subscribe(() => { this.toast.success('action.restored'); this.load(); }); }, - messageParams: { entity: group.name }, + messageParams: { entity: domain.name }, }); } } diff --git a/web/src/app/modules/admin/groups/form-page/group-form-page.component.html b/web/src/app/modules/admin/groups/form-page/group-form-page.component.html index 82d8e33..150c081 100644 --- a/web/src/app/modules/admin/groups/form-page/group-form-page.component.html +++ b/web/src/app/modules/admin/groups/form-page/group-form-page.component.html @@ -27,5 +27,13 @@ type="text" formControlName="name"/>
+
+
diff --git a/web/src/app/modules/admin/groups/form-page/group-form-page.component.ts b/web/src/app/modules/admin/groups/form-page/group-form-page.component.ts index e86ca33..a462d6a 100644 --- a/web/src/app/modules/admin/groups/form-page/group-form-page.component.ts +++ b/web/src/app/modules/admin/groups/form-page/group-form-page.component.ts @@ -1,18 +1,20 @@ -import { Component } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ToastService } from "src/app/service/toast.service"; -import { FormPageBase } from "src/app/core/base/form-page-base"; +import { Component } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ToastService } from 'src/app/service/toast.service'; +import { FormPageBase } from 'src/app/core/base/form-page-base'; import { Group, GroupCreateInput, GroupUpdateInput, -} from "src/app/model/entities/group"; -import { GroupsDataService } from "src/app/modules/admin/groups/groups.data.service"; +} from 'src/app/model/entities/group'; +import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service'; +import { Role } from 'src/app/model/entities/role'; +import { CommonDataService } from 'src/app/modules/shared/service/common-data.service'; @Component({ - selector: "app-group-form-page", - templateUrl: "./group-form-page.component.html", - styleUrl: "./group-form-page.component.scss", + selector: 'app-group-form-page', + templateUrl: './group-form-page.component.html', + styleUrl: './group-form-page.component.scss', }) export class GroupFormPageComponent extends FormPageBase< Group, @@ -20,8 +22,17 @@ export class GroupFormPageComponent extends FormPageBase< GroupUpdateInput, GroupsDataService > { - constructor(private toast: ToastService) { + roles: Role[] = []; + + constructor( + private toast: ToastService, + private cds: CommonDataService + ) { super(); + this.cds.getAllRoles().subscribe(roles => { + this.roles = roles; + }); + if (!this.nodeId) { this.node = this.new(); this.setForm(this.node); @@ -31,7 +42,7 @@ export class GroupFormPageComponent extends FormPageBase< this.dataService .load([{ id: { equal: this.nodeId } }]) - .subscribe((apiKey) => { + .subscribe(apiKey => { this.node = apiKey.nodes[0]; this.setForm(this.node); }); @@ -45,40 +56,48 @@ export class GroupFormPageComponent extends FormPageBase< this.form = new FormGroup({ id: new FormControl(undefined), name: new FormControl(undefined, Validators.required), + roles: new FormControl([]), }); - this.form.controls["id"].disable(); + this.form.controls['id'].disable(); } setForm(node?: Group) { - this.form.controls["id"].setValue(node?.id); - this.form.controls["name"].setValue(node?.name); + this.form.controls['id'].setValue(node?.id); + this.form.controls['name'].setValue(node?.name); + this.form.controls['roles'].setValue(node?.roles ?? []); } getCreateInput(): GroupCreateInput { return { - name: this.form.controls["name"].pristine + name: this.form.controls['name'].pristine ? undefined - : (this.form.controls["name"].value ?? undefined), + : (this.form.controls['name'].value ?? undefined), + roles: this.form.controls['roles'].pristine + ? undefined + : this.form.controls['roles'].value, }; } getUpdateInput(): GroupUpdateInput { 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, - name: this.form.controls["name"].pristine + id: this.form.controls['id'].value, + name: this.form.controls['name'].pristine ? undefined - : (this.form.controls["name"].value ?? undefined), + : (this.form.controls['name'].value ?? undefined), + roles: this.form.controls['roles'].pristine + ? undefined + : this.form.controls['roles'].value, }; } create(apiKey: GroupCreateInput): void { this.dataService.create(apiKey).subscribe(() => { this.spinner.hide(); - this.toast.success("action.created"); + this.toast.success('action.created'); this.close(); }); } @@ -86,7 +105,7 @@ export class GroupFormPageComponent extends FormPageBase< update(apiKey: GroupUpdateInput): void { this.dataService.update(apiKey).subscribe(() => { this.spinner.hide(); - this.toast.success("action.created"); + this.toast.success('action.created'); this.close(); }); } diff --git a/web/src/app/modules/admin/groups/groups.data.service.ts b/web/src/app/modules/admin/groups/groups.data.service.ts index d2620e0..40f5d21 100644 --- a/web/src/app/modules/admin/groups/groups.data.service.ts +++ b/web/src/app/modules/admin/groups/groups.data.service.ts @@ -1,24 +1,24 @@ -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 { 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 { 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 { Group, GroupCreateInput, GroupUpdateInput, -} from "src/app/model/entities/group"; +} from 'src/app/model/entities/group'; @Injectable() export class GroupsDataService @@ -31,7 +31,7 @@ export class GroupsDataService { constructor( private spinner: SpinnerService, - private apollo: Apollo, + private apollo: Apollo ) { super(); } @@ -40,7 +40,7 @@ export class GroupsDataService filter?: Filter[] | undefined, sort?: Sort[] | undefined, skip?: number | undefined, - take?: number | undefined, + take?: number | undefined ): Observable> { return this.apollo .query<{ groups: QueryResult }>({ @@ -57,6 +57,10 @@ export class GroupsDataService nodes { id name + roles { + id + name + } ...DB_MODEL } @@ -73,12 +77,12 @@ export class GroupsDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data.groups)); + .pipe(map(result => result.data.groups)); } loadById(id: number): Observable { @@ -89,6 +93,10 @@ export class GroupsDataService group(filter: { id: { equal: $id } }) { id name + roles { + id + name + } ...DB_MODEL } @@ -101,12 +109,12 @@ export class GroupsDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data.groups.nodes[0])); + .pipe(map(result => result.data.groups.nodes[0])); } create(object: GroupCreateInput): Observable { @@ -129,16 +137,17 @@ export class GroupsDataService variables: { input: { name: object.name, + roles: object.roles?.map(x => x.id), }, }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.group.create)); + .pipe(map(result => result.data?.group.create)); } update(object: GroupUpdateInput): Observable { @@ -162,16 +171,17 @@ export class GroupsDataService input: { id: object.id, name: object.name, + roles: object.roles?.map(x => x.id), }, }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.group.update)); + .pipe(map(result => result.data?.group.update)); } delete(object: Group): Observable { @@ -189,12 +199,12 @@ export class GroupsDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.group.delete ?? false)); + .pipe(map(result => result.data?.group.delete ?? false)); } restore(object: Group): Observable { @@ -212,12 +222,12 @@ export class GroupsDataService }, }) .pipe( - catchError((err) => { + catchError(err => { this.spinner.hide(); throw err; - }), + }) ) - .pipe(map((result) => result.data?.group.restore ?? false)); + .pipe(map(result => result.data?.group.restore ?? false)); } static provide(): Provider[] { diff --git a/web/src/app/modules/admin/short-urls/short-urls.page.html b/web/src/app/modules/admin/short-urls/short-urls.page.html index 6d87a17..1a54a6c 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.page.html +++ b/web/src/app/modules/admin/short-urls/short-urls.page.html @@ -109,7 +109,7 @@
-
+
diff --git a/web/src/app/modules/shared/service/common-data.service.ts b/web/src/app/modules/shared/service/common-data.service.ts new file mode 100644 index 0000000..90b22ba --- /dev/null +++ b/web/src/app/modules/shared/service/common-data.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Role } from 'src/app/model/entities/role'; +import { QueryResult } from 'src/app/model/entities/query-result'; +import { Apollo, gql } from 'apollo-angular'; +import { catchError, map } from 'rxjs/operators'; +import { SpinnerService } from 'src/app/service/spinner.service'; + +@Injectable({ + providedIn: 'root', +}) +export class CommonDataService { + constructor( + private spinner: SpinnerService, + private apollo: Apollo + ) {} + + getAllRoles(): Observable { + return this.apollo + .query<{ roles: QueryResult }>({ + query: gql` + query getRoles { + roles { + nodes { + id + name + } + } + } + `, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data.roles.nodes)); + } +} diff --git a/web/src/app/service/auth.service.ts b/web/src/app/service/auth.service.ts index c4a939d..fe7943b 100644 --- a/web/src/app/service/auth.service.ts +++ b/web/src/app/service/auth.service.ts @@ -1,16 +1,16 @@ -import { Injectable } from "@angular/core"; -import { User } from "src/app/model/auth/user"; -import { BehaviorSubject, firstValueFrom, Observable } from "rxjs"; -import { Apollo, gql } from "apollo-angular"; -import { PermissionsEnum } from "src/app/model/auth/permissionsEnum"; -import { KeycloakService } from "keycloak-angular"; -import { map } from "rxjs/operators"; -import { Logger } from "src/app/service/logger.service"; +import { Injectable } from '@angular/core'; +import { User } from 'src/app/model/auth/user'; +import { BehaviorSubject, concatWith, firstValueFrom, Observable } from 'rxjs'; +import { Apollo, gql } from 'apollo-angular'; +import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; +import { KeycloakService } from 'keycloak-angular'; +import { map } from 'rxjs/operators'; +import { Logger } from 'src/app/service/logger.service'; -const log = new Logger("AuthService"); +const log = new Logger('AuthService'); @Injectable({ - providedIn: "root", + providedIn: 'root', }) export class AuthService { protected anyPermissionForAdminPage = [ @@ -23,7 +23,7 @@ export class AuthService { constructor( private apollo: Apollo, - private keycloakService: KeycloakService, + private keycloakService: KeycloakService ) {} private requestUser() { @@ -60,11 +60,11 @@ export class AuthService { permission, }, }) - .pipe(map((result) => result.data.userHasPermission)); + .pipe(map(result => result.data.userHasPermission)); } private userHasAnyPermission( - permissions: PermissionsEnum[], + permissions: PermissionsEnum[] ): Observable { return this.apollo .query<{ userHasAnyPermission: boolean }>({ @@ -77,7 +77,7 @@ export class AuthService { permissions, }, }) - .pipe(map((result) => result.data.userHasAnyPermission)); + .pipe(map(result => result.data.userHasAnyPermission)); } loadUser() { @@ -88,8 +88,8 @@ export class AuthService { return; } - this.requestUser().subscribe((result) => { - log.info("User loaded"); + this.requestUser().subscribe(result => { + log.info('User loaded'); this.user$.next(result.data.user); }); } @@ -111,18 +111,15 @@ export class AuthService { if (!this.user$.value) return false; const userPermissions = this.user$.value.roles - .map((role) => (role.permissions ?? []).map((p) => p.name)) + .map(role => (role.permissions ?? []).map(p => p.name)) .flat(); - return permissions.every((permission) => - userPermissions.includes(permission), - ); + return permissions.some(permission => userPermissions.includes(permission)); } async hasAnyPermissionLazy(permissions: PermissionsEnum[]): Promise { if (this.user$.value && this.user$.value.roles) { return this.hasAnyPermission(permissions); } - return await firstValueFrom(this.userHasAnyPermission(permissions)); } @@ -130,7 +127,7 @@ export class AuthService { if (!this.user$.value) return false; const permissions = this.user$.value.roles - .map((role) => (role.permissions ?? []).map((p) => p.name)) + .map(role => (role.permissions ?? []).map(p => p.name)) .flat(); return permissions.includes(permission); } diff --git a/web/src/app/service/error-handling.service.ts b/web/src/app/service/error-handling.service.ts index 1688cd8..802994a 100644 --- a/web/src/app/service/error-handling.service.ts +++ b/web/src/app/service/error-handling.service.ts @@ -1,22 +1,22 @@ -import { TranslateService } from "@ngx-translate/core"; -import { Logger } from "src/app/service/logger.service"; -import { ErrorHandler, Injectable } from "@angular/core"; -import { ToastService } from "src/app/service/toast.service"; -import { HttpErrorResponse } from "@angular/common/http"; -import { ApolloError } from "@apollo/client/errors"; -import { MissingPermissionException } from "src/app/model/utils/error"; -import { GraphQLError } from "graphql/error/GraphQLError"; -import { ToastOptions } from "src/app/model/utils/toast-options"; +import { TranslateService } from '@ngx-translate/core'; +import { Logger } from 'src/app/service/logger.service'; +import { ErrorHandler, Injectable } from '@angular/core'; +import { ToastService } from 'src/app/service/toast.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ApolloError } from '@apollo/client/errors'; +import { MissingPermissionException } from 'src/app/model/utils/error'; +import { GraphQLError } from 'graphql/error/GraphQLError'; +import { ToastOptions } from 'src/app/model/utils/toast-options'; -const logger = new Logger("ErrorHandler"); +const logger = new Logger('ErrorHandler'); @Injectable({ - providedIn: "root", + providedIn: 'root', }) export class ErrorHandlingService implements ErrorHandler { constructor( private t: TranslateService, - private toast: ToastService, + private toast: ToastService ) {} handleError(error: HttpErrorResponse | ApolloError) { @@ -30,7 +30,7 @@ export class ErrorHandlingService implements ErrorHandler { return; } console.error(error); - // this.handleHttpError(error); + this.handleHttpError(error); } private handleHttpError(e: HttpErrorResponse) { @@ -40,14 +40,14 @@ export class ErrorHandlingService implements ErrorHandler { error?: string; options?: ToastOptions; } = { - summary: this.t.instant("common.error"), + summary: this.t.instant('common.error'), detail: e.message, }; if (e.status === 401) { toast = { - summary: this.t.instant("common.error"), - detail: this.t.instant("error.unauthorized"), + summary: this.t.instant('common.error'), + detail: this.t.instant('error.unauthorized'), }; } @@ -57,15 +57,15 @@ export class ErrorHandlingService implements ErrorHandler { .permissions; toast = { - summary: this.t.instant("common.error"), - detail: this.t.instant("error.missing_permissions", { - permissions: missingPermissions.join(", "), + summary: this.t.instant('common.error'), + detail: this.t.instant('error.missing_permissions', { + permissions: missingPermissions.join(', '), }), }; } else { toast = { - summary: this.t.instant("common.error"), - detail: this.t.instant("error.permission_denied"), + summary: this.t.instant('common.error'), + detail: this.t.instant('error.permission_denied'), }; } } @@ -73,7 +73,7 @@ export class ErrorHandlingService implements ErrorHandler { if (e.status === 500 && e.error && e.error.errors) { if (e.error.errors.length > 0) { toast = { - summary: this.t.instant("common.api_error"), + summary: this.t.instant('common.api_error'), detail: e.error.errors[0].message, }; } @@ -86,8 +86,8 @@ export class ErrorHandlingService implements ErrorHandler { private handleGraphQlErrors(errors: GraphQLError[]) { errors.forEach((e: GraphQLError) => { this.toast.error( - this.t.instant("common.api_error"), - `${e.message}${e.path ? " " + e.path.join(".") : ""}`, + this.t.instant('common.api_error'), + `${e.message}${e.path ? ' ' + e.path.join('.') : ''}` ); }); } diff --git a/web/src/app/service/sidebar.service.ts b/web/src/app/service/sidebar.service.ts index 028b87c..c92b12c 100644 --- a/web/src/app/service/sidebar.service.ts +++ b/web/src/app/service/sidebar.service.ts @@ -50,6 +50,7 @@ export class SidebarService { routerLink: ['/admin/urls'], visible: await this.auth.hasAnyPermissionLazy([ PermissionsEnum.shortUrls, + PermissionsEnum.shortUrlsByAssignment, ]), }, await this.groupAdministration(), diff --git a/web/src/assets/i18n/de.json b/web/src/assets/i18n/de.json index bf6e6ba..06861fd 100644 --- a/web/src/assets/i18n/de.json +++ b/web/src/assets/i18n/de.json @@ -116,6 +116,7 @@ "roles.delete": "Löschen", "roles.update": "Bearbeiten", "short_urls": "Kurz-URLs", + "short_urls.by_assignment": "Zuweisung", "short_urls.create": "Erstellen", "short_urls.delete": "Löschen", "short_urls.update": "Bearbeiten", @@ -127,6 +128,11 @@ "qr": { "width": "Breite" }, + "permission_descriptions": { + "users.update": "Benutzer inkl. der Rollen ändern", + "short_urls": "Alle URLs sehen", + "short_urls.by_assignment": "Alle Kurz-URLs anzeigen, die einer Gruppe nach Rolle zugewiesen sind" + }, "role": { "count_header": "Rolle(n)" }, diff --git a/web/src/assets/i18n/en.json b/web/src/assets/i18n/en.json index c4e5169..75129b7 100644 --- a/web/src/assets/i18n/en.json +++ b/web/src/assets/i18n/en.json @@ -116,6 +116,7 @@ "roles.delete": "Delete", "roles.update": "Update", "short_urls": "Short URLs", + "short_urls.by_assignment": "By assignment", "short_urls.create": "Create", "short_urls.delete": "Delete", "short_urls.update": "Update", @@ -124,6 +125,11 @@ "users.delete": "Delete", "users.update": "Update" }, + "permission_descriptions": { + "users.update": "Change users including their roles", + "short_urls": "See all URLs", + "short_urls.by_assignment": "See all short urls assigned to a group by role" + }, "qr": { "width": "Width" },