[WIP] Handle per user setup
All checks were successful
Test API before pr merge / test-lint (pull_request) Successful in 10s
Test before pr merge / test-translation-lint (pull_request) Successful in 39s
Test before pr merge / test-lint (pull_request) Successful in 41s
Test before pr merge / test-before-merge (pull_request) Successful in 1m39s
All checks were successful
Test API before pr merge / test-lint (pull_request) Successful in 10s
Test before pr merge / test-translation-lint (pull_request) Successful in 39s
Test before pr merge / test-lint (pull_request) Successful in 41s
Test before pr merge / test-before-merge (pull_request) Successful in 1m39s
This commit is contained in:
parent
0ed3cb846d
commit
e378393813
@ -1,7 +1,6 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import Type, Union
|
from typing import Type, Union
|
||||||
|
|
||||||
from api_graphql.abc.input_abc import InputABC
|
|
||||||
from api_graphql.abc.query_abc import QueryABC
|
from api_graphql.abc.query_abc import QueryABC
|
||||||
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
||||||
from core.database.abc.data_access_object_abc import DataAccessObjectABC
|
from core.database.abc.data_access_object_abc import DataAccessObjectABC
|
||||||
@ -22,6 +21,7 @@ class MutationABC(QueryABC):
|
|||||||
name: str,
|
name: str,
|
||||||
mutation_name: str,
|
mutation_name: str,
|
||||||
require_any_permission=None,
|
require_any_permission=None,
|
||||||
|
require_any=None,
|
||||||
public: bool = False,
|
public: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -29,24 +29,30 @@ class MutationABC(QueryABC):
|
|||||||
:param str name: GraphQL mutation name
|
:param str name: GraphQL mutation name
|
||||||
:param str mutation_name: Internal (class) mutation name without "Mutation" suffix
|
:param str mutation_name: Internal (class) mutation name without "Mutation" suffix
|
||||||
:param list[Permissions] require_any_permission: List of permissions required to access the field
|
:param list[Permissions] require_any_permission: List of permissions required to access the field
|
||||||
|
:param tuple[list[Permissions], list[callable]] require_any: List of permissions and resolvers required to access the field
|
||||||
:param bool public: Define if the field can resolve without authentication
|
:param bool public: Define if the field can resolve without authentication
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if require_any_permission is None:
|
|
||||||
require_any_permission = []
|
|
||||||
from api_graphql.definition import QUERIES
|
from api_graphql.definition import QUERIES
|
||||||
|
|
||||||
self.field(
|
field = (
|
||||||
MutationFieldBuilder(name)
|
MutationFieldBuilder(name)
|
||||||
.with_resolver(
|
.with_resolver(
|
||||||
lambda *args, **kwargs: [
|
lambda *args, **kwargs: [
|
||||||
x for x in QUERIES if x.name == f"{mutation_name}Mutation"
|
x for x in QUERIES if x.name == f"{mutation_name}Mutation"
|
||||||
][0]
|
][0]
|
||||||
)
|
)
|
||||||
.with_require_any_permission(require_any_permission)
|
|
||||||
.with_public(public)
|
.with_public(public)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if require_any_permission is not None:
|
||||||
|
field.with_require_any_permission(require_any_permission)
|
||||||
|
|
||||||
|
if require_any is not None:
|
||||||
|
field.with_require_any(*require_any)
|
||||||
|
|
||||||
|
self.field(field)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _resolve_assignments(
|
async def _resolve_assignments(
|
||||||
foreign_objs: list[int],
|
foreign_objs: list[int],
|
||||||
|
@ -79,6 +79,7 @@ class QueryABC(ObjectType):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
resolver_results = []
|
||||||
for x in resolvers:
|
for x in resolvers:
|
||||||
user = await Route.get_authenticated_user_or_api_key_or_default()
|
user = await Route.get_authenticated_user_or_api_key_or_default()
|
||||||
user_permissions = []
|
user_permissions = []
|
||||||
@ -86,13 +87,15 @@ class QueryABC(ObjectType):
|
|||||||
user_permissions = await user.permissions
|
user_permissions = await user.permissions
|
||||||
|
|
||||||
if iscoroutinefunction(x):
|
if iscoroutinefunction(x):
|
||||||
result = await x(
|
resolver_results.append(
|
||||||
QueryContext(data, user, user_permissions, *args, **kwargs)
|
await x(QueryContext(data, user, user_permissions, *args, **kwargs))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = x(QueryContext(data, user, user_permissions, *args, **kwargs))
|
resolver_results.append(
|
||||||
|
x(QueryContext(data, user, user_permissions, *args, **kwargs))
|
||||||
|
)
|
||||||
|
|
||||||
if not result:
|
if not any(resolver_results):
|
||||||
raise AccessDenied()
|
raise AccessDenied()
|
||||||
|
|
||||||
def field(
|
def field(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from api_graphql.abc.mutation_abc import MutationABC
|
from api_graphql.abc.mutation_abc import MutationABC
|
||||||
|
from api_graphql.require_any_resolvers import by_user_setup_mutation
|
||||||
from service.permission.permissions_enum import Permissions
|
from service.permission.permissions_enum import Permissions
|
||||||
|
|
||||||
|
|
||||||
@ -54,11 +55,14 @@ class Mutation(MutationABC):
|
|||||||
self.add_mutation_type(
|
self.add_mutation_type(
|
||||||
"shortUrl",
|
"shortUrl",
|
||||||
"ShortUrl",
|
"ShortUrl",
|
||||||
require_any_permission=[
|
require_any=(
|
||||||
|
[
|
||||||
Permissions.short_urls_create,
|
Permissions.short_urls_create,
|
||||||
Permissions.short_urls_update,
|
Permissions.short_urls_update,
|
||||||
Permissions.short_urls_delete,
|
Permissions.short_urls_delete,
|
||||||
],
|
],
|
||||||
|
[by_user_setup_mutation],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.add_mutation_type(
|
self.add_mutation_type(
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from api.route import Route
|
from api.route import Route
|
||||||
from api_graphql.abc.mutation_abc import MutationABC
|
from api_graphql.abc.mutation_abc import MutationABC
|
||||||
|
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
||||||
from api_graphql.input.short_url_create_input import ShortUrlCreateInput
|
from api_graphql.input.short_url_create_input import ShortUrlCreateInput
|
||||||
from api_graphql.input.short_url_update_input import ShortUrlUpdateInput
|
from api_graphql.input.short_url_update_input import ShortUrlUpdateInput
|
||||||
|
from api_graphql.require_any_resolvers import by_user_setup_mutation
|
||||||
from core.configuration.feature_flags import FeatureFlags
|
from core.configuration.feature_flags import FeatureFlags
|
||||||
from core.configuration.feature_flags_enum import FeatureFlagsEnum
|
from core.configuration.feature_flags_enum import FeatureFlagsEnum
|
||||||
from core.logger import APILogger
|
from core.logger import APILogger
|
||||||
@ -20,32 +22,36 @@ class ShortUrlMutation(MutationABC):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
MutationABC.__init__(self, "ShortUrl")
|
MutationABC.__init__(self, "ShortUrl")
|
||||||
|
|
||||||
self.mutation(
|
self.field(
|
||||||
"create",
|
MutationFieldBuilder("create")
|
||||||
self.resolve_create,
|
.with_resolver(self.resolve_create)
|
||||||
ShortUrlCreateInput,
|
.with_input(ShortUrlCreateInput)
|
||||||
require_any_permission=[Permissions.short_urls_create],
|
.with_require_any([Permissions.short_urls_create], [by_user_setup_mutation])
|
||||||
)
|
)
|
||||||
self.mutation(
|
|
||||||
"update",
|
self.field(
|
||||||
self.resolve_update,
|
MutationFieldBuilder("update")
|
||||||
ShortUrlUpdateInput,
|
.with_resolver(self.resolve_update)
|
||||||
require_any_permission=[Permissions.short_urls_update],
|
.with_input(ShortUrlUpdateInput)
|
||||||
|
.with_require_any([Permissions.short_urls_update], [by_user_setup_mutation])
|
||||||
)
|
)
|
||||||
self.mutation(
|
|
||||||
"delete",
|
self.field(
|
||||||
self.resolve_delete,
|
MutationFieldBuilder("delete")
|
||||||
require_any_permission=[Permissions.short_urls_delete],
|
.with_resolver(self.resolve_delete)
|
||||||
|
.with_require_any([Permissions.short_urls_delete], [by_user_setup_mutation])
|
||||||
)
|
)
|
||||||
self.mutation(
|
|
||||||
"restore",
|
self.field(
|
||||||
self.resolve_restore,
|
MutationFieldBuilder("restore")
|
||||||
require_any_permission=[Permissions.short_urls_delete],
|
.with_resolver(self.resolve_restore)
|
||||||
|
.with_require_any([Permissions.short_urls_delete], [by_user_setup_mutation])
|
||||||
)
|
)
|
||||||
self.mutation(
|
|
||||||
"trackVisit",
|
self.field(
|
||||||
self.resolve_track_visit,
|
MutationFieldBuilder("trackVisit")
|
||||||
require_any_permission=[Permissions.short_urls_update],
|
.with_resolver(self.resolve_track_visit)
|
||||||
|
.with_require_any_permission([Permissions.short_urls_update])
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -131,12 +131,12 @@ class Query(QueryABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
|
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
|
||||||
group_field = group_field.with_default_filter(self._resolve_default_user_filter)
|
group_field = group_field.with_default_filter(
|
||||||
|
self._resolve_default_user_filter
|
||||||
self.field(
|
|
||||||
group_field
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.field(group_field)
|
||||||
|
|
||||||
short_url_field = (
|
short_url_field = (
|
||||||
DaoFieldBuilder("shortUrls")
|
DaoFieldBuilder("shortUrls")
|
||||||
.with_dao(shortUrlDao)
|
.with_dao(shortUrlDao)
|
||||||
@ -149,12 +149,12 @@ class Query(QueryABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
|
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
|
||||||
short_url_field = short_url_field.with_default_filter(self._resolve_default_user_filter)
|
short_url_field = short_url_field.with_default_filter(
|
||||||
|
self._resolve_default_user_filter
|
||||||
self.field(
|
|
||||||
short_url_field
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.field(short_url_field)
|
||||||
|
|
||||||
self.field(
|
self.field(
|
||||||
ResolverFieldBuilder("settings")
|
ResolverFieldBuilder("settings")
|
||||||
.with_resolver(self._resolve_settings)
|
.with_resolver(self._resolve_settings)
|
||||||
|
@ -37,3 +37,10 @@ async def by_user_setup_resolver(ctx: QueryContext) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return all(x.user_setup_id == ctx.user.id for x in ctx.data.nodes)
|
return all(x.user_setup_id == ctx.user.id for x in ctx.data.nodes)
|
||||||
|
|
||||||
|
|
||||||
|
async def by_user_setup_mutation(ctx: QueryContext) -> bool:
|
||||||
|
if not FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return all(x.user_setup_id == ctx.user.id for x in ctx.data.nodes)
|
||||||
|
@ -44,7 +44,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
|||||||
self._default_filter_condition = None
|
self._default_filter_condition = None
|
||||||
|
|
||||||
self.__attributes: dict[str, type] = {}
|
self.__attributes: dict[str, type] = {}
|
||||||
self.__joins: dict[str, str] = {}
|
self.__joins: list[str] = []
|
||||||
|
|
||||||
self.__db_names: dict[str, str] = {}
|
self.__db_names: dict[str, str] = {}
|
||||||
self.__foreign_tables: dict[str, str] = {}
|
self.__foreign_tables: dict[str, str] = {}
|
||||||
@ -138,7 +138,21 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
|||||||
if table_name == self._table_name:
|
if table_name == self._table_name:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.__joins[foreign_attr] = (
|
if any(f"{table_name} ON" in join for join in self.__joins):
|
||||||
|
index = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i, join in enumerate(self.__joins)
|
||||||
|
if f"{table_name} ON" in join
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if index is not None:
|
||||||
|
self.__joins[
|
||||||
|
index
|
||||||
|
] += f" AND {table_name}.{primary_attr} = {self._table_name}.{foreign_attr}"
|
||||||
|
else:
|
||||||
|
self.__joins.append(
|
||||||
f"LEFT JOIN {table_name} ON {table_name}.{primary_attr} = {self._table_name}.{foreign_attr}"
|
f"LEFT JOIN {table_name} ON {table_name}.{primary_attr} = {self._table_name}.{foreign_attr}"
|
||||||
)
|
)
|
||||||
self.__foreign_tables[attr] = table_name
|
self.__foreign_tables[attr] = table_name
|
||||||
@ -168,8 +182,8 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
|||||||
|
|
||||||
async def count(self, filters: AttributeFilters = None) -> int:
|
async def count(self, filters: AttributeFilters = None) -> int:
|
||||||
query = f"SELECT COUNT(*) FROM {self._table_name}"
|
query = f"SELECT COUNT(*) FROM {self._table_name}"
|
||||||
for join in self.__joins:
|
join_str = f" ".join(self.__joins)
|
||||||
query += f" {self.__joins[join]}"
|
query += f" {join_str}" if len(join_str) > 0 else ""
|
||||||
|
|
||||||
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
|
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
|
||||||
conditions, external_table_deps = await self._build_conditions(filters)
|
conditions, external_table_deps = await self._build_conditions(filters)
|
||||||
@ -192,8 +206,9 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
|||||||
without_deleted=False,
|
without_deleted=False,
|
||||||
) -> list[T_DBM]:
|
) -> list[T_DBM]:
|
||||||
query = f"SELECT {self._table_name}_history.* FROM {self._table_name}_history"
|
query = f"SELECT {self._table_name}_history.* FROM {self._table_name}_history"
|
||||||
|
|
||||||
for join in self.__joins:
|
for join in self.__joins:
|
||||||
query += f" {self.__joins[join].replace(self._table_name, f'{self._table_name}_history')}"
|
query += f" {join.replace(self._table_name, f'{self._table_name}_history')}"
|
||||||
|
|
||||||
query += f" WHERE {f'{self._table_name}_history.{self.__primary_key}' if by_key is None else f'{self._table_name}_history.{by_key}'} = {entry_id}"
|
query += f" WHERE {f'{self._table_name}_history.{self.__primary_key}' if by_key is None else f'{self._table_name}_history.{by_key}'} = {entry_id}"
|
||||||
|
|
||||||
@ -593,8 +608,8 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
|||||||
|
|
||||||
external_table_deps = []
|
external_table_deps = []
|
||||||
query = f"SELECT {self._table_name}.* FROM {self._table_name}"
|
query = f"SELECT {self._table_name}.* FROM {self._table_name}"
|
||||||
for join in self.__joins:
|
join_str = f" ".join(self.__joins)
|
||||||
query += f" {self.__joins[join]}"
|
query += f" {join_str}" if len(join_str) > 0 else ""
|
||||||
|
|
||||||
# Collect dependencies from filters
|
# Collect dependencies from filters
|
||||||
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
|
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
|
||||||
|
@ -54,7 +54,7 @@ class DbModelABC(ABC):
|
|||||||
|
|
||||||
from data.schemas.administration.user_dao import userDao
|
from data.schemas.administration.user_dao import userDao
|
||||||
|
|
||||||
return await userDao.find_single_by({"id": self._editor_id})
|
return await userDao.get_by_id(self._editor_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created(self) -> datetime:
|
def created(self) -> datetime:
|
||||||
@ -63,3 +63,7 @@ class DbModelABC(ABC):
|
|||||||
@property
|
@property
|
||||||
def updated(self) -> datetime:
|
def updated(self) -> datetime:
|
||||||
return self._updated
|
return self._updated
|
||||||
|
|
||||||
|
@updated.setter
|
||||||
|
def updated(self, value: datetime):
|
||||||
|
self._updated = value
|
||||||
|
@ -14,6 +14,15 @@ class DbModelDaoABC[T_DBM](DataAccessObjectABC[T_DBM]):
|
|||||||
|
|
||||||
self.attribute(DbModelABC.id, int, ignore=True)
|
self.attribute(DbModelABC.id, int, ignore=True)
|
||||||
self.attribute(DbModelABC.deleted, bool)
|
self.attribute(DbModelABC.deleted, bool)
|
||||||
self.attribute(DbModelABC.editor_id, int, ignore=True)
|
self.attribute(DbModelABC.editor_id, int, ignore=True) # handled by db trigger
|
||||||
self.attribute(DbModelABC.created, datetime, "created", ignore=True)
|
|
||||||
self.attribute(DbModelABC.updated, datetime, "updated", ignore=True)
|
self.reference(
|
||||||
|
"editor", "id", DbModelABC.editor_id, "administration.users"
|
||||||
|
) # not relevant for updates due to editor_id
|
||||||
|
|
||||||
|
self.attribute(
|
||||||
|
DbModelABC.created, datetime, ignore=True
|
||||||
|
) # handled by db trigger
|
||||||
|
self.attribute(
|
||||||
|
DbModelABC.updated, datetime, ignore=True
|
||||||
|
) # handled by db trigger
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
from core.database.database_settings import DatabaseSettings
|
|
||||||
from core.database.db_context import DBContext
|
from core.database.db_context import DBContext
|
||||||
from core.environment import Environment
|
|
||||||
from core.logger import DBLogger
|
|
||||||
|
|
||||||
logger = DBLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
@ -19,30 +14,3 @@ class Database:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def connect(cls):
|
def connect(cls):
|
||||||
cls._db.connect()
|
cls._db.connect()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def startup_db(cls):
|
|
||||||
from data.service.migration_service import MigrationService
|
|
||||||
|
|
||||||
logger.info("Init DB")
|
|
||||||
db = DBContext()
|
|
||||||
host = Environment.get("DB_HOST", str)
|
|
||||||
port = Environment.get("DB_PORT", int)
|
|
||||||
user = Environment.get("DB_USER", str)
|
|
||||||
password = Environment.get("DB_PASSWORD", str)
|
|
||||||
database = Environment.get("DB_DATABASE", str)
|
|
||||||
|
|
||||||
if None in [host, port, user, password, database]:
|
|
||||||
logger.fatal(
|
|
||||||
"DB settings are not set correctly",
|
|
||||||
EnvironmentError("DB settings are not set correctly"),
|
|
||||||
)
|
|
||||||
|
|
||||||
await db.connect(
|
|
||||||
DatabaseSettings(
|
|
||||||
host=host, port=port, user=user, password=password, database=database
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Database.init(db)
|
|
||||||
migrations = MigrationService(db)
|
|
||||||
await migrations.migrate()
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
from psycopg import OperationalError
|
||||||
|
from psycopg_pool import PoolTimeout
|
||||||
|
|
||||||
from core.database.database_settings import DatabaseSettings
|
from core.database.database_settings import DatabaseSettings
|
||||||
from core.database.postgres_pool import PostgresPool
|
from core.database.postgres_pool import PostgresPool
|
||||||
from core.environment import Environment
|
from core.environment import Environment
|
||||||
@ -26,15 +29,15 @@ class DBContext:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.fatal("Connecting to database failed", e)
|
logger.fatal("Connecting to database failed", e)
|
||||||
|
|
||||||
async def execute(self, statement: str, args=None) -> list[list]:
|
async def execute(self, statement: str, args=None, multi=True) -> list[list]:
|
||||||
logger.trace(f"execute {statement} with args: {args}")
|
logger.trace(f"execute {statement} with args: {args}")
|
||||||
return await self._pool.execute(statement, args)
|
return await self._pool.execute(statement, args, multi)
|
||||||
|
|
||||||
async def select_map(self, statement: str, args=None) -> list[dict]:
|
async def select_map(self, statement: str, args=None) -> list[dict]:
|
||||||
logger.trace(f"select {statement} with args: {args}")
|
logger.trace(f"select {statement} with args: {args}")
|
||||||
try:
|
try:
|
||||||
return await self._pool.select_map(statement, args)
|
return await self._pool.select_map(statement, args)
|
||||||
except Exception as e:
|
except (OperationalError, PoolTimeout) as e:
|
||||||
if self._fails >= 3:
|
if self._fails >= 3:
|
||||||
logger.error(f"Database error caused by {statement}", e)
|
logger.error(f"Database error caused by {statement}", e)
|
||||||
uid = uuid.uuid4()
|
uid = uuid.uuid4()
|
||||||
@ -50,6 +53,9 @@ class DBContext:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
return []
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database error caused by {statement}", e)
|
||||||
|
raise e
|
||||||
|
|
||||||
async def select(
|
async def select(
|
||||||
self, statement: str, args=None
|
self, statement: str, args=None
|
||||||
@ -57,7 +63,7 @@ class DBContext:
|
|||||||
logger.trace(f"select {statement} with args: {args}")
|
logger.trace(f"select {statement} with args: {args}")
|
||||||
try:
|
try:
|
||||||
return await self._pool.select(statement, args)
|
return await self._pool.select(statement, args)
|
||||||
except Exception as e:
|
except (OperationalError, PoolTimeout) as e:
|
||||||
if self._fails >= 3:
|
if self._fails >= 3:
|
||||||
logger.error(f"Database error caused by {statement}", e)
|
logger.error(f"Database error caused by {statement}", e)
|
||||||
uid = uuid.uuid4()
|
uid = uuid.uuid4()
|
||||||
@ -73,3 +79,6 @@ class DBContext:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
return []
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database error caused by {statement}", e)
|
||||||
|
raise e
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional, Any
|
||||||
|
|
||||||
from psycopg import sql
|
from psycopg import sql
|
||||||
from psycopg_pool import AsyncConnectionPool, PoolTimeout
|
from psycopg_pool import AsyncConnectionPool, PoolTimeout
|
||||||
@ -21,7 +21,7 @@ class PostgresPool:
|
|||||||
f"host={database_settings.host} "
|
f"host={database_settings.host} "
|
||||||
f"port={database_settings.port} "
|
f"port={database_settings.port} "
|
||||||
f"user={database_settings.user} "
|
f"user={database_settings.user} "
|
||||||
f"password={B64Helper.decode(database_settings.password)} "
|
f"password={database_settings.password} "
|
||||||
f"dbname={database_settings.database}"
|
f"dbname={database_settings.database}"
|
||||||
)
|
)
|
||||||
self._pool_size = pool_size
|
self._pool_size = pool_size
|
||||||
@ -41,18 +41,31 @@ class PostgresPool:
|
|||||||
logger.fatal(f"Failed to connect to the database", e)
|
logger.fatal(f"Failed to connect to the database", e)
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
async def execute(self, query: str, args=None) -> list[list]:
|
@staticmethod
|
||||||
|
async def _exec_sql(cursor: Any, query: str, args=None, multi=True):
|
||||||
|
if multi:
|
||||||
|
queries = query.split(";")
|
||||||
|
for q in queries:
|
||||||
|
if q.strip() == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
await cursor.execute(sql.SQL(q), args)
|
||||||
|
else:
|
||||||
|
await cursor.execute(sql.SQL(query), args)
|
||||||
|
|
||||||
|
async def execute(self, query: str, args=None, multi=True) -> list[list]:
|
||||||
"""
|
"""
|
||||||
Execute a SQL statement, it could be with args and without args. The usage is
|
Execute a SQL statement, it could be with args and without args. The usage is
|
||||||
similar to the execute() function in the psycopg module.
|
similar to the execute() function in the psycopg module.
|
||||||
:param query: SQL clause
|
:param query: SQL clause
|
||||||
:param args: args needed by the SQL clause
|
:param args: args needed by the SQL clause
|
||||||
|
:param multi: if the query is a multi-statement
|
||||||
:return: return result
|
:return: return result
|
||||||
"""
|
"""
|
||||||
async with await self._get_pool() as pool:
|
async with await self._get_pool() as pool:
|
||||||
async with pool.connection() as con:
|
async with pool.connection() as con:
|
||||||
async with con.cursor() as cursor:
|
async with con.cursor() as cursor:
|
||||||
await cursor.execute(sql.SQL(query), args)
|
await self._exec_sql(cursor, query, args, multi)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cursor.description is not None
|
cursor.description is not None
|
||||||
@ -68,34 +81,36 @@ class PostgresPool:
|
|||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def select(self, query: str, args=None) -> list[str]:
|
async def select(self, query: str, args=None, multi=True) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Execute a SQL statement, it could be with args and without args. The usage is
|
Execute a SQL statement, it could be with args and without args. The usage is
|
||||||
similar to the execute() function in the psycopg module.
|
similar to the execute() function in the psycopg module.
|
||||||
:param query: SQL clause
|
:param query: SQL clause
|
||||||
:param args: args needed by the SQL clause
|
:param args: args needed by the SQL clause
|
||||||
|
:param multi: if the query is a multi-statement
|
||||||
:return: return result
|
:return: return result
|
||||||
"""
|
"""
|
||||||
async with await self._get_pool() as pool:
|
async with await self._get_pool() as pool:
|
||||||
async with pool.connection() as con:
|
async with pool.connection() as con:
|
||||||
async with con.cursor() as cursor:
|
async with con.cursor() as cursor:
|
||||||
await cursor.execute(sql.SQL(query), args)
|
await self._exec_sql(cursor, query, args, multi)
|
||||||
|
|
||||||
res = await cursor.fetchall()
|
res = await cursor.fetchall()
|
||||||
return list(res)
|
return list(res)
|
||||||
|
|
||||||
async def select_map(self, query: str, args=None) -> list[dict]:
|
async def select_map(self, query: str, args=None, multi=True) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Execute a SQL statement, it could be with args and without args. The usage is
|
Execute a SQL statement, it could be with args and without args. The usage is
|
||||||
similar to the execute() function in the psycopg module.
|
similar to the execute() function in the psycopg module.
|
||||||
:param query: SQL clause
|
:param query: SQL clause
|
||||||
:param args: args needed by the SQL clause
|
:param args: args needed by the SQL clause
|
||||||
|
:param multi: if the query is a multi-statement
|
||||||
:return: return result
|
:return: return result
|
||||||
"""
|
"""
|
||||||
async with await self._get_pool() as pool:
|
async with await self._get_pool() as pool:
|
||||||
async with pool.connection() as con:
|
async with pool.connection() as con:
|
||||||
async with con.cursor() as cursor:
|
async with con.cursor() as cursor:
|
||||||
await cursor.execute(sql.SQL(query), args)
|
await self._exec_sql(cursor, query, args, multi)
|
||||||
|
|
||||||
res = await cursor.fetchall()
|
res = await cursor.fetchall()
|
||||||
res_map: list[dict] = []
|
res_map: list[dict] = []
|
||||||
|
@ -77,7 +77,7 @@ class MigrationService:
|
|||||||
|
|
||||||
logger.debug(f"Running upgrade migration: {migration.name}")
|
logger.debug(f"Running upgrade migration: {migration.name}")
|
||||||
|
|
||||||
await self._db.execute(migration.script)
|
await self._db.execute(migration.script, multi=False)
|
||||||
|
|
||||||
await executedMigrationDao.create(
|
await executedMigrationDao.create(
|
||||||
ExecutedMigration(migration.name), skip_editor=True
|
ExecutedMigration(migration.name), skip_editor=True
|
||||||
|
@ -4,6 +4,7 @@ import { Logger } from 'src/app/service/logger.service';
|
|||||||
import { ToastService } from 'src/app/service/toast.service';
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
import { AuthService } from 'src/app/service/auth.service';
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||||
|
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||||
|
|
||||||
const log = new Logger('PermissionGuard');
|
const log = new Logger('PermissionGuard');
|
||||||
|
|
||||||
@ -14,11 +15,17 @@ export class PermissionGuard {
|
|||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private toast: ToastService,
|
private toast: ToastService,
|
||||||
private auth: AuthService
|
private auth: AuthService,
|
||||||
|
private features: FeatureFlagService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
|
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||||
const permissions = route.data['permissions'] as PermissionsEnum[];
|
const permissions = route.data['permissions'] as PermissionsEnum[];
|
||||||
|
const checkByPerUserSetup = route.data['checkByPerUserSetup'] as boolean;
|
||||||
|
|
||||||
|
if (checkByPerUserSetup) {
|
||||||
|
return await this.features.get('PerUserSetup');
|
||||||
|
}
|
||||||
|
|
||||||
if (!permissions || permissions.length === 0) {
|
if (!permissions || permissions.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -22,7 +22,7 @@ const routes: Routes = [
|
|||||||
m => m.GroupsModule
|
m => m.GroupsModule
|
||||||
),
|
),
|
||||||
canActivate: [PermissionGuard],
|
canActivate: [PermissionGuard],
|
||||||
data: { permissions: [PermissionsEnum.groups] },
|
data: { permissions: [PermissionsEnum.groups], checkByPerUserSetup: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'urls',
|
path: 'urls',
|
||||||
@ -36,6 +36,7 @@ const routes: Routes = [
|
|||||||
PermissionsEnum.shortUrls,
|
PermissionsEnum.shortUrls,
|
||||||
PermissionsEnum.shortUrlsByAssignment,
|
PermissionsEnum.shortUrlsByAssignment,
|
||||||
],
|
],
|
||||||
|
checkByPerUserSetup: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
></p-dropdown>
|
></p-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-page-input">
|
<div class="form-page-input" *ngIf="!isPerUserSetup">
|
||||||
<p class="label">{{ 'common.domain' | translate }}</p>
|
<p class="label">{{ 'common.domain' | translate }}</p>
|
||||||
<div
|
<div
|
||||||
class="value">
|
class="value">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { ToastService } from 'src/app/service/toast.service';
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
import { FormPageBase } from 'src/app/core/base/form-page-base';
|
import { FormPageBase } from 'src/app/core/base/form-page-base';
|
||||||
@ -10,29 +10,45 @@ import {
|
|||||||
import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-urls.data.service';
|
import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-urls.data.service';
|
||||||
import { Group } from 'src/app/model/entities/group';
|
import { Group } from 'src/app/model/entities/group';
|
||||||
import { Domain } from 'src/app/model/entities/domain';
|
import { Domain } from 'src/app/model/entities/domain';
|
||||||
|
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-short-url-form-page',
|
selector: 'app-short-url-form-page',
|
||||||
templateUrl: './short-url-form-page.component.html',
|
templateUrl: './short-url-form-page.component.html',
|
||||||
styleUrl: './short-url-form-page.component.scss',
|
styleUrl: './short-url-form-page.component.scss',
|
||||||
})
|
})
|
||||||
export class ShortUrlFormPageComponent extends FormPageBase<
|
export class ShortUrlFormPageComponent
|
||||||
|
extends FormPageBase<
|
||||||
ShortUrl,
|
ShortUrl,
|
||||||
ShortUrlCreateInput,
|
ShortUrlCreateInput,
|
||||||
ShortUrlUpdateInput,
|
ShortUrlUpdateInput,
|
||||||
ShortUrlsDataService
|
ShortUrlsDataService
|
||||||
> {
|
>
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
groups: Group[] = [];
|
groups: Group[] = [];
|
||||||
domains: Domain[] = [];
|
domains: Domain[] = [];
|
||||||
|
|
||||||
constructor(private toast: ToastService) {
|
isPerUserSetup = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private features: FeatureFlagService,
|
||||||
|
private toast: ToastService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.dataService.getAllGroups().subscribe(groups => {
|
this.dataService.getAllGroups().subscribe(groups => {
|
||||||
this.groups = groups;
|
this.groups = groups;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.isPerUserSetup = await this.features.get('PerUserSetup');
|
||||||
|
|
||||||
|
if (!this.isPerUserSetup) {
|
||||||
this.dataService.getAllDomains().subscribe(domains => {
|
this.dataService.getAllDomains().subscribe(domains => {
|
||||||
this.domains = domains;
|
this.domains = domains;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.nodeId) {
|
if (!this.nodeId) {
|
||||||
this.node = this.new();
|
this.node = this.new();
|
||||||
|
@ -22,6 +22,7 @@ const routes: Routes = [
|
|||||||
canActivate: [PermissionGuard],
|
canActivate: [PermissionGuard],
|
||||||
data: {
|
data: {
|
||||||
permissions: [PermissionsEnum.shortUrlsCreate],
|
permissions: [PermissionsEnum.shortUrlsCreate],
|
||||||
|
checkByPerUserSetup: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -30,6 +31,7 @@ const routes: Routes = [
|
|||||||
canActivate: [PermissionGuard],
|
canActivate: [PermissionGuard],
|
||||||
data: {
|
data: {
|
||||||
permissions: [PermissionsEnum.shortUrlsUpdate],
|
permissions: [PermissionsEnum.shortUrlsUpdate],
|
||||||
|
checkByPerUserSetup: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -37,7 +39,8 @@ const routes: Routes = [
|
|||||||
component: HistoryComponent,
|
component: HistoryComponent,
|
||||||
canActivate: [PermissionGuard],
|
canActivate: [PermissionGuard],
|
||||||
data: {
|
data: {
|
||||||
permissions: [PermissionsEnum.domains],
|
permissions: [PermissionsEnum.shortUrls],
|
||||||
|
checkByPerUserSetup: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -12,6 +12,7 @@ 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';
|
||||||
import { ResolvedTableColumn } from 'src/app/modules/shared/components/table/table.model';
|
import { ResolvedTableColumn } from 'src/app/modules/shared/components/table/table.model';
|
||||||
|
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-short-urls',
|
selector: 'app-short-urls',
|
||||||
@ -67,7 +68,8 @@ export class ShortUrlsPage
|
|||||||
private toast: ToastService,
|
private toast: ToastService,
|
||||||
private confirmation: ConfirmationDialogService,
|
private confirmation: ConfirmationDialogService,
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private config: ConfigService
|
private config: ConfigService,
|
||||||
|
private features: FeatureFlagService
|
||||||
) {
|
) {
|
||||||
super(true, {
|
super(true, {
|
||||||
read: [PermissionsEnum.shortUrls],
|
read: [PermissionsEnum.shortUrls],
|
||||||
@ -80,21 +82,26 @@ export class ShortUrlsPage
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.hasPermissions = {
|
this.hasPermissions = {
|
||||||
read: await this.auth.hasAnyPermissionLazy(
|
read:
|
||||||
|
(await this.auth.hasAnyPermissionLazy(
|
||||||
this.requiredPermissions.read ?? []
|
this.requiredPermissions.read ?? []
|
||||||
),
|
)) || (await this.features.get('PerUserSetup')),
|
||||||
create: await this.auth.hasAnyPermissionLazy(
|
create:
|
||||||
|
(await this.auth.hasAnyPermissionLazy(
|
||||||
this.requiredPermissions.create ?? []
|
this.requiredPermissions.create ?? []
|
||||||
),
|
)) || (await this.features.get('PerUserSetup')),
|
||||||
update: await this.auth.hasAnyPermissionLazy(
|
update:
|
||||||
|
(await this.auth.hasAnyPermissionLazy(
|
||||||
this.requiredPermissions.update ?? []
|
this.requiredPermissions.update ?? []
|
||||||
),
|
)) || (await this.features.get('PerUserSetup')),
|
||||||
delete: await this.auth.hasAnyPermissionLazy(
|
delete:
|
||||||
|
(await this.auth.hasAnyPermissionLazy(
|
||||||
this.requiredPermissions.delete ?? []
|
this.requiredPermissions.delete ?? []
|
||||||
),
|
)) || (await this.features.get('PerUserSetup')),
|
||||||
restore: await this.auth.hasAnyPermissionLazy(
|
restore:
|
||||||
|
(await this.auth.hasAnyPermissionLazy(
|
||||||
this.requiredPermissions.restore ?? []
|
this.requiredPermissions.restore ?? []
|
||||||
),
|
)) || (await this.features.get('PerUserSetup')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user