Formatting
Some checks failed
Test before pr merge / test-lint (pull_request) Successful in 44s
Test before pr merge / test-translation-lint (pull_request) Successful in 40s
Test before pr merge / test-before-merge (pull_request) Failing after 1m38s

This commit is contained in:
Sven Heidemann 2025-03-08 09:50:01 +01:00
parent 86bd5fb545
commit 7de31f5d32
12 changed files with 128 additions and 108 deletions

View File

@ -36,15 +36,15 @@ class Route(RouteUserExtension):
@classmethod @classmethod
async def _get_auth_type( async def _get_auth_type(
cls, request: Request, auth_header: str cls, request: Request, auth_header: str
) -> Optional[Union[User, ApiKey]]: ) -> Optional[Union[User, ApiKey]]:
if auth_header.startswith("Bearer "): if auth_header.startswith("Bearer "):
return await cls.get_user() return await cls.get_user()
elif auth_header.startswith("API-Key "): elif auth_header.startswith("API-Key "):
return await cls.get_api_key(request) return await cls.get_api_key(request)
elif ( elif (
auth_header.startswith("DEV-User ") auth_header.startswith("DEV-User ")
and Environment.get_environment() == "development" and Environment.get_environment() == "development"
): ):
return await cls.get_dev_user() return await cls.get_dev_user()
return None return None
@ -66,7 +66,7 @@ class Route(RouteUserExtension):
@classmethod @classmethod
async def get_authenticated_user_or_api_key_or_default( async def get_authenticated_user_or_api_key_or_default(
cls, cls,
) -> Optional[Union[User, ApiKey]]: ) -> Optional[Union[User, ApiKey]]:
request = get_request() request = get_request()
if request is None: if request is None:
@ -93,8 +93,8 @@ class Route(RouteUserExtension):
elif auth_header.startswith("API-Key "): elif auth_header.startswith("API-Key "):
return await cls._verify_api_key(request) return await cls._verify_api_key(request)
elif ( elif (
auth_header.startswith("DEV-User ") auth_header.startswith("DEV-User ")
and Environment.get_environment() == "development" and Environment.get_environment() == "development"
): ):
user = await cls.get_dev_user() user = await cls.get_dev_user()
return user is not None return user is not None
@ -102,10 +102,10 @@ class Route(RouteUserExtension):
@classmethod @classmethod
def authorize( def authorize(
cls, cls,
f: Callable = None, f: Callable = None,
skip_in_dev=False, skip_in_dev=False,
by_api_key=False, by_api_key=False,
): ):
if f is None: if f is None:
return functools.partial( return functools.partial(

View File

@ -13,11 +13,11 @@ class MutationABC(QueryABC):
QueryABC.__init__(self, f"{name}Mutation") QueryABC.__init__(self, f"{name}Mutation")
def add_mutation_type( def add_mutation_type(
self, self,
name: str, name: str,
mutation_name: str, mutation_name: str,
require_any_permission=None, require_any_permission=None,
public: bool = False, public: bool = False,
): ):
""" """
Add mutation type (sub mutation) to the mutation object Add mutation type (sub mutation) to the mutation object

View File

@ -64,19 +64,19 @@ class QueryABC(ObjectType):
@classmethod @classmethod
async def _require_any( async def _require_any(
cls, cls,
data: Any, data: Any,
permissions: TRequireAnyPermissions, permissions: TRequireAnyPermissions,
resolvers: TRequireAnyResolvers, resolvers: TRequireAnyResolvers,
*args, *args,
**kwargs, **kwargs,
): ):
info = args[0] info = args[0]
if len(permissions) > 0: if len(permissions) > 0:
user = await Route.get_authenticated_user_or_api_key_or_default() user = await Route.get_authenticated_user_or_api_key_or_default()
if user is not None and all( if user is not None and all(
[await user.has_permission(x) for x in permissions] [await user.has_permission(x) for x in permissions]
): ):
return return
@ -97,13 +97,13 @@ class QueryABC(ObjectType):
raise AccessDenied() raise AccessDenied()
def field( def field(
self, self,
builder: Union[ builder: Union[
DaoFieldBuilder, DaoFieldBuilder,
CollectionFieldBuilder, CollectionFieldBuilder,
ResolverFieldBuilder, ResolverFieldBuilder,
MutationFieldBuilder, MutationFieldBuilder,
], ],
): ):
""" """
Add a field to the query Add a field to the query
@ -199,7 +199,7 @@ class QueryABC(ObjectType):
elif isinstance(field, MutationField): elif isinstance(field, MutationField):
async def input_wrapper( async def input_wrapper(
mutation: QueryABC, info: GraphQLResolveInfo, **kwargs mutation: QueryABC, info: GraphQLResolveInfo, **kwargs
): ):
if field.input_type is None: if field.input_type is None:
return await resolver_wrapper(mutation, info, **kwargs) return await resolver_wrapper(mutation, info, **kwargs)
@ -230,9 +230,9 @@ class QueryABC(ObjectType):
await self._authorize() await self._authorize()
if ( if (
field.require_any is None field.require_any is None
and not field.public and not field.public
and field.require_any_permission and field.require_any_permission
): ):
await self._require_any_permission(field.require_any_permission) await self._require_any_permission(field.require_any_permission)
@ -252,13 +252,13 @@ class QueryABC(ObjectType):
@deprecated("Use field(FieldBuilder()) instead") @deprecated("Use field(FieldBuilder()) instead")
def mutation( def mutation(
self, self,
name: str, name: str,
f: Callable, f: Callable,
input_type: Type[InputABC] = None, input_type: Type[InputABC] = None,
input_key: str = "input", input_key: str = "input",
require_any_permission: list[Permissions] = None, require_any_permission: list[Permissions] = None,
public: bool = False, public: bool = False,
): ):
""" """
Adds a mutation to the query Adds a mutation to the query
@ -284,13 +284,13 @@ class QueryABC(ObjectType):
@classmethod @classmethod
def _resolve_collection( def _resolve_collection(
cls, cls,
collection: list, collection: list,
*_, *_,
filters: list[CollectionFilterABC] = None, filters: list[CollectionFilterABC] = None,
sort: list[Sort] = None, sort: list[Sort] = None,
skip: int = None, skip: int = None,
take: int = None, take: int = None,
) -> CollectionResult: ) -> CollectionResult:
total_count = len(collection) total_count = len(collection)
@ -313,7 +313,7 @@ class QueryABC(ObjectType):
return attr return attr
for s in reversed( for s in reversed(
sort sort
): # Apply sorting in reverse order to make first primary "orderBy" and other secondary "thenBy" ): # 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("_")] attrs = [a for a in dir(s) if not a.startswith("_")]
for k in attrs: for k in attrs:

View File

@ -16,9 +16,12 @@ class GroupQuery(DbModelQueryABC):
self.field( self.field(
ResolverFieldBuilder("shortUrls") ResolverFieldBuilder("shortUrls")
.with_resolver(self._get_urls) .with_resolver(self._get_urls)
.with_require_any([ .with_require_any(
Permissions.groups, [
], [group_by_assignment_resolver]) Permissions.groups,
],
[group_by_assignment_resolver],
)
) )
self.set_field("roles", self._get_roles) self.set_field("roles", self._get_roles)

View File

@ -120,7 +120,7 @@ class Query(QueryABC):
Permissions.short_urls_create, Permissions.short_urls_create,
Permissions.short_urls_update, Permissions.short_urls_update,
], ],
[group_by_assignment_resolver] [group_by_assignment_resolver],
) )
) )
self.field( self.field(

View File

@ -12,11 +12,19 @@ async def group_by_assignment_resolver(ctx: QueryContext) -> bool:
groups = [await x.group for x in ctx.data.nodes] groups = [await x.group for x in ctx.data.nodes]
role_ids = {x.id for x in await ctx.user.roles} role_ids = {x.id for x in await ctx.user.roles}
filtered_groups = [ filtered_groups = [
g.id for g in groups if g.id
g is not None and (roles := await groupDao.get_roles(g.id)) and all(r.id in role_ids for r in roles) 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] 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
return True return True

View File

@ -9,7 +9,7 @@ TRequireAnyResolvers = list[
Union[ Union[
Callable[[QueryContext], bool], Callable[[QueryContext], bool],
Awaitable[[QueryContext], bool], Awaitable[[QueryContext], bool],
Callable[[QueryContext], Coroutine[Any, Any, bool]] Callable[[QueryContext], Coroutine[Any, Any, bool]],
] ]
] ]
TRequireAny = tuple[TRequireAnyPermissions, TRequireAnyResolvers] TRequireAny = tuple[TRequireAnyPermissions, TRequireAnyResolvers]

View File

@ -42,12 +42,12 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return self._table_name return self._table_name
def attribute( def attribute(
self, self,
attr_name: Attribute, attr_name: Attribute,
attr_type: type, attr_type: type,
db_name: str = None, db_name: str = None,
ignore=False, ignore=False,
primary_key=False, primary_key=False,
): ):
""" """
Add an attribute for db and object mapping to the data access object Add an attribute for db and object mapping to the data access object
@ -77,11 +77,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
self.__date_attributes.add(db_name) self.__date_attributes.add(db_name)
def reference( def reference(
self, self,
attr: Attribute, attr: Attribute,
primary_attr: Attribute, primary_attr: Attribute,
foreign_attr: Attribute, foreign_attr: Attribute,
table_name: str, table_name: str,
): ):
""" """
Add a reference to another table for the given attribute Add a reference to another table for the given attribute
@ -164,11 +164,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return self.to_object(result[0]) return self.to_object(result[0])
async def get_by( async def get_by(
self, self,
filters: AttributeFilters = None, filters: AttributeFilters = None,
sorts: AttributeSorts = None, sorts: AttributeSorts = None,
take: int = None, take: int = None,
skip: int = None, skip: int = None,
) -> list[T_DBM]: ) -> list[T_DBM]:
""" """
Get all objects by the given filters Get all objects by the given filters
@ -189,11 +189,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return [self.to_object(x) for x in result] return [self.to_object(x) for x in result]
async def get_single_by( async def get_single_by(
self, self,
filters: AttributeFilters = None, filters: AttributeFilters = None,
sorts: AttributeSorts = None, sorts: AttributeSorts = None,
take: int = None, take: int = None,
skip: int = None, skip: int = None,
) -> T_DBM: ) -> T_DBM:
""" """
Get a single object by the given filters Get a single object by the given filters
@ -214,11 +214,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return result[0] return result[0]
async def find_by( async def find_by(
self, self,
filters: AttributeFilters = None, filters: AttributeFilters = None,
sorts: AttributeSorts = None, sorts: AttributeSorts = None,
take: int = None, take: int = None,
skip: int = None, skip: int = None,
) -> list[Optional[T_DBM]]: ) -> list[Optional[T_DBM]]:
""" """
Find all objects by the given filters Find all objects by the given filters
@ -238,11 +238,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return [self.to_object(x) for x in result] return [self.to_object(x) for x in result]
async def find_single_by( async def find_single_by(
self, self,
filters: AttributeFilters = None, filters: AttributeFilters = None,
sorts: AttributeSorts = None, sorts: AttributeSorts = None,
take: int = None, take: int = None,
skip: int = None, skip: int = None,
) -> Optional[T_DBM]: ) -> Optional[T_DBM]:
""" """
Find a single object by the given filters Find a single object by the given filters
@ -342,7 +342,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
await self._db.execute(query) await self._db.execute(query)
async def _build_delete_statement( async def _build_delete_statement(
self, obj: T_DBM, hard_delete: bool = False self, obj: T_DBM, hard_delete: bool = False
) -> str: ) -> str:
if hard_delete: if hard_delete:
return f""" return f"""
@ -458,11 +458,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return cast_type(value) return cast_type(value)
def _build_conditional_query( def _build_conditional_query(
self, self,
filters: AttributeFilters = None, filters: AttributeFilters = None,
sorts: AttributeSorts = None, sorts: AttributeSorts = None,
take: int = None, take: int = None,
skip: int = None, skip: int = None,
) -> str: ) -> str:
query = f"SELECT {self._table_name}.* FROM {self._table_name}" query = f"SELECT {self._table_name}.* FROM {self._table_name}"
@ -506,7 +506,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
" OR ".join( " OR ".join(
self._build_fuzzy_conditions( self._build_fuzzy_conditions(
[ [
self.__db_names[x] if x in self.__db_names else self.__db_names[camel_to_snake(x)] (
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]) for x in get_value(values, "fields", list[str])
], ],
get_value(values, "term", str), get_value(values, "term", str),
@ -546,7 +550,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return " AND ".join(conditions) return " AND ".join(conditions)
def _build_fuzzy_conditions( def _build_fuzzy_conditions(
self, fields: list[str], term: str, threshold: int = 10 self, fields: list[str], term: str, threshold: int = 10
) -> list[str]: ) -> list[str]:
conditions = [] conditions = []
for field in fields: for field in fields:

View File

@ -4,11 +4,11 @@ from core.typing import T
def get_value( def get_value(
source: dict, source: dict,
key: str, key: str,
cast_type: Type[T], cast_type: Type[T],
default: Optional[T] = None, default: Optional[T] = None,
list_delimiter: str = ",", list_delimiter: str = ",",
) -> Optional[T]: ) -> Optional[T]:
""" """
Get value from source dictionary and cast it to a specified type. Get value from source dictionary and cast it to a specified type.
@ -26,8 +26,8 @@ def get_value(
value = source[key] value = source[key]
if isinstance( if isinstance(
value, value,
cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__, cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__,
): ):
return value return value
@ -36,10 +36,15 @@ def get_value(
return value.lower() in ["true", "1"] return value.lower() in ["true", "1"]
if ( if (
cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__ cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__
) == list: ) == list:
if not (value.startswith("[") and value.endswith("]")) and list_delimiter not in value: if (
raise ValueError("List values must be enclosed in square brackets or use a delimiter.") not (value.startswith("[") and value.endswith("]"))
and list_delimiter not in value
):
raise ValueError(
"List values must be enclosed in square brackets or use a delimiter."
)
if value.startswith("[") and value.endswith("]"): if value.startswith("[") and value.endswith("]"):
value = value[1:-1] value = value[1:-1]

View File

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

View File

@ -8,7 +8,6 @@ import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-url
import { ShortUrlsColumns } from 'src/app/modules/admin/short-urls/short-urls.columns'; import { ShortUrlsColumns } from 'src/app/modules/admin/short-urls/short-urls.columns';
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from 'src/app/service/auth.service';
import { Filter } from 'src/app/model/graphql/filter/filter.model'; import { Filter } from 'src/app/model/graphql/filter/filter.model';
import { SettingsService } from 'src/app/service/settings.service';
import QrCodeWithLogo from 'qrcode-with-logos'; import QrCodeWithLogo from 'qrcode-with-logos';
import { FileUpload, FileUploadHandlerEvent } from 'primeng/fileupload'; import { FileUpload, FileUploadHandlerEvent } from 'primeng/fileupload';
import { ConfigService } from 'src/app/service/config.service'; import { ConfigService } from 'src/app/service/config.service';

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { User } from 'src/app/model/auth/user'; import { User } from 'src/app/model/auth/user';
import { BehaviorSubject, concatWith, firstValueFrom, Observable } from 'rxjs'; import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { Apollo, gql } from 'apollo-angular'; import { Apollo, gql } from 'apollo-angular';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';