From e3783938133fa9d6236382c44fd9428a158403d1 Mon Sep 17 00:00:00 2001 From: edraft Date: Mon, 21 Apr 2025 18:12:21 +0200 Subject: [PATCH] [WIP] Handle per user setup --- api/src/api_graphql/abc/mutation_abc.py | 16 ++++-- api/src/api_graphql/abc/query_abc.py | 13 +++-- api/src/api_graphql/mutation.py | 14 ++++-- .../mutations/short_url_mutation.py | 50 +++++++++++-------- api/src/api_graphql/query.py | 16 +++--- api/src/api_graphql/require_any_resolvers.py | 7 +++ .../database/abc/data_access_object_abc.py | 33 ++++++++---- api/src/core/database/abc/db_model_abc.py | 6 ++- api/src/core/database/abc/db_model_dao_abc.py | 15 ++++-- api/src/core/database/database.py | 32 ------------ api/src/core/database/db_context.py | 17 +++++-- api/src/core/database/postgres_pool.py | 31 +++++++++--- api/src/data/service/migration_service.py | 2 +- web/src/app/core/guard/permission.guard.ts | 9 +++- web/src/app/modules/admin/admin.module.ts | 3 +- .../short-url-form-page.component.html | 2 +- .../short-url-form-page.component.ts | 38 ++++++++++---- .../admin/short-urls/short-urls.module.ts | 5 +- .../admin/short-urls/short-urls.page.ts | 39 +++++++++------ 19 files changed, 214 insertions(+), 134 deletions(-) diff --git a/api/src/api_graphql/abc/mutation_abc.py b/api/src/api_graphql/abc/mutation_abc.py index 59c97ff..95e61f4 100644 --- a/api/src/api_graphql/abc/mutation_abc.py +++ b/api/src/api_graphql/abc/mutation_abc.py @@ -1,7 +1,6 @@ from abc import abstractmethod from typing import Type, Union -from api_graphql.abc.input_abc import InputABC from api_graphql.abc.query_abc import QueryABC from api_graphql.field.mutation_field_builder import MutationFieldBuilder from core.database.abc.data_access_object_abc import DataAccessObjectABC @@ -22,6 +21,7 @@ class MutationABC(QueryABC): name: str, mutation_name: str, require_any_permission=None, + require_any=None, public: bool = False, ): """ @@ -29,24 +29,30 @@ class MutationABC(QueryABC): :param str name: GraphQL mutation name :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 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 :return: """ - if require_any_permission is None: - require_any_permission = [] from api_graphql.definition import QUERIES - self.field( + field = ( MutationFieldBuilder(name) .with_resolver( lambda *args, **kwargs: [ x for x in QUERIES if x.name == f"{mutation_name}Mutation" ][0] ) - .with_require_any_permission(require_any_permission) .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 async def _resolve_assignments( foreign_objs: list[int], diff --git a/api/src/api_graphql/abc/query_abc.py b/api/src/api_graphql/abc/query_abc.py index f9865b9..0ea4d6b 100644 --- a/api/src/api_graphql/abc/query_abc.py +++ b/api/src/api_graphql/abc/query_abc.py @@ -79,6 +79,7 @@ class QueryABC(ObjectType): ): return + resolver_results = [] for x in resolvers: user = await Route.get_authenticated_user_or_api_key_or_default() user_permissions = [] @@ -86,14 +87,16 @@ class QueryABC(ObjectType): user_permissions = await user.permissions if iscoroutinefunction(x): - result = await x( - QueryContext(data, user, user_permissions, *args, **kwargs) + resolver_results.append( + await x(QueryContext(data, user, user_permissions, *args, **kwargs)) ) else: - result = x(QueryContext(data, user, user_permissions, *args, **kwargs)) + resolver_results.append( + x(QueryContext(data, user, user_permissions, *args, **kwargs)) + ) - if not result: - raise AccessDenied() + if not any(resolver_results): + raise AccessDenied() def field( self, diff --git a/api/src/api_graphql/mutation.py b/api/src/api_graphql/mutation.py index d46df60..59b2a5c 100644 --- a/api/src/api_graphql/mutation.py +++ b/api/src/api_graphql/mutation.py @@ -1,4 +1,5 @@ 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 @@ -54,11 +55,14 @@ class Mutation(MutationABC): self.add_mutation_type( "shortUrl", "ShortUrl", - require_any_permission=[ - Permissions.short_urls_create, - Permissions.short_urls_update, - Permissions.short_urls_delete, - ], + require_any=( + [ + Permissions.short_urls_create, + Permissions.short_urls_update, + Permissions.short_urls_delete, + ], + [by_user_setup_mutation], + ), ) self.add_mutation_type( diff --git a/api/src/api_graphql/mutations/short_url_mutation.py b/api/src/api_graphql/mutations/short_url_mutation.py index d229eaf..f39cefa 100644 --- a/api/src/api_graphql/mutations/short_url_mutation.py +++ b/api/src/api_graphql/mutations/short_url_mutation.py @@ -1,7 +1,9 @@ from api.route import Route 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_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_enum import FeatureFlagsEnum from core.logger import APILogger @@ -20,32 +22,36 @@ class ShortUrlMutation(MutationABC): def __init__(self): MutationABC.__init__(self, "ShortUrl") - self.mutation( - "create", - self.resolve_create, - ShortUrlCreateInput, - require_any_permission=[Permissions.short_urls_create], + self.field( + MutationFieldBuilder("create") + .with_resolver(self.resolve_create) + .with_input(ShortUrlCreateInput) + .with_require_any([Permissions.short_urls_create], [by_user_setup_mutation]) ) - self.mutation( - "update", - self.resolve_update, - ShortUrlUpdateInput, - require_any_permission=[Permissions.short_urls_update], + + self.field( + MutationFieldBuilder("update") + .with_resolver(self.resolve_update) + .with_input(ShortUrlUpdateInput) + .with_require_any([Permissions.short_urls_update], [by_user_setup_mutation]) ) - self.mutation( - "delete", - self.resolve_delete, - require_any_permission=[Permissions.short_urls_delete], + + self.field( + MutationFieldBuilder("delete") + .with_resolver(self.resolve_delete) + .with_require_any([Permissions.short_urls_delete], [by_user_setup_mutation]) ) - self.mutation( - "restore", - self.resolve_restore, - require_any_permission=[Permissions.short_urls_delete], + + self.field( + MutationFieldBuilder("restore") + .with_resolver(self.resolve_restore) + .with_require_any([Permissions.short_urls_delete], [by_user_setup_mutation]) ) - self.mutation( - "trackVisit", - self.resolve_track_visit, - require_any_permission=[Permissions.short_urls_update], + + self.field( + MutationFieldBuilder("trackVisit") + .with_resolver(self.resolve_track_visit) + .with_require_any_permission([Permissions.short_urls_update]) ) @staticmethod diff --git a/api/src/api_graphql/query.py b/api/src/api_graphql/query.py index 0b9908e..0f949f5 100644 --- a/api/src/api_graphql/query.py +++ b/api/src/api_graphql/query.py @@ -131,11 +131,11 @@ class Query(QueryABC): ) 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 = ( DaoFieldBuilder("shortUrls") @@ -149,11 +149,11 @@ class Query(QueryABC): ) 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( ResolverFieldBuilder("settings") diff --git a/api/src/api_graphql/require_any_resolvers.py b/api/src/api_graphql/require_any_resolvers.py index 2032dcf..e688fd9 100644 --- a/api/src/api_graphql/require_any_resolvers.py +++ b/api/src/api_graphql/require_any_resolvers.py @@ -37,3 +37,10 @@ async def by_user_setup_resolver(ctx: QueryContext) -> bool: return False 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) 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 4a8092f..230b77b 100644 --- a/api/src/core/database/abc/data_access_object_abc.py +++ b/api/src/core/database/abc/data_access_object_abc.py @@ -44,7 +44,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): self._default_filter_condition = None self.__attributes: dict[str, type] = {} - self.__joins: dict[str, str] = {} + self.__joins: list[str] = [] self.__db_names: dict[str, str] = {} self.__foreign_tables: dict[str, str] = {} @@ -138,9 +138,23 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): if table_name == self._table_name: return - self.__joins[foreign_attr] = ( - f"LEFT JOIN {table_name} ON {table_name}.{primary_attr} = {self._table_name}.{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}" + ) self.__foreign_tables[attr] = table_name def use_external_fields(self, builder: ExternalDataTempTableBuilder): @@ -168,8 +182,8 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): async def count(self, filters: AttributeFilters = None) -> int: query = f"SELECT COUNT(*) FROM {self._table_name}" - for join in self.__joins: - query += f" {self.__joins[join]}" + join_str = f" ".join(self.__joins) + query += f" {join_str}" if len(join_str) > 0 else "" if filters is not None and (not isinstance(filters, list) or len(filters) > 0): conditions, external_table_deps = await self._build_conditions(filters) @@ -192,8 +206,9 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): without_deleted=False, ) -> list[T_DBM]: query = f"SELECT {self._table_name}_history.* FROM {self._table_name}_history" + 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}" @@ -593,8 +608,8 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): external_table_deps = [] query = f"SELECT {self._table_name}.* FROM {self._table_name}" - for join in self.__joins: - query += f" {self.__joins[join]}" + join_str = f" ".join(self.__joins) + query += f" {join_str}" if len(join_str) > 0 else "" # Collect dependencies from filters if filters is not None and (not isinstance(filters, list) or len(filters) > 0): diff --git a/api/src/core/database/abc/db_model_abc.py b/api/src/core/database/abc/db_model_abc.py index eddf74f..b8c81d6 100644 --- a/api/src/core/database/abc/db_model_abc.py +++ b/api/src/core/database/abc/db_model_abc.py @@ -54,7 +54,7 @@ class DbModelABC(ABC): 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 def created(self) -> datetime: @@ -63,3 +63,7 @@ class DbModelABC(ABC): @property def updated(self) -> datetime: return self._updated + + @updated.setter + def updated(self, value: datetime): + self._updated = value diff --git a/api/src/core/database/abc/db_model_dao_abc.py b/api/src/core/database/abc/db_model_dao_abc.py index 9d8d638..79be8f2 100644 --- a/api/src/core/database/abc/db_model_dao_abc.py +++ b/api/src/core/database/abc/db_model_dao_abc.py @@ -14,6 +14,15 @@ class DbModelDaoABC[T_DBM](DataAccessObjectABC[T_DBM]): self.attribute(DbModelABC.id, int, ignore=True) self.attribute(DbModelABC.deleted, bool) - self.attribute(DbModelABC.editor_id, int, ignore=True) - self.attribute(DbModelABC.created, datetime, "created", ignore=True) - self.attribute(DbModelABC.updated, datetime, "updated", ignore=True) + self.attribute(DbModelABC.editor_id, int, ignore=True) # handled by db trigger + + 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 diff --git a/api/src/core/database/database.py b/api/src/core/database/database.py index 36adc9f..347dc65 100644 --- a/api/src/core/database/database.py +++ b/api/src/core/database/database.py @@ -1,9 +1,4 @@ -from core.database.database_settings import DatabaseSettings from core.database.db_context import DBContext -from core.environment import Environment -from core.logger import DBLogger - -logger = DBLogger(__name__) class Database: @@ -19,30 +14,3 @@ class Database: @classmethod def connect(cls): 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() diff --git a/api/src/core/database/db_context.py b/api/src/core/database/db_context.py index fb2d5a5..7e363c9 100644 --- a/api/src/core/database/db_context.py +++ b/api/src/core/database/db_context.py @@ -1,6 +1,9 @@ import uuid from typing import Optional, Any +from psycopg import OperationalError +from psycopg_pool import PoolTimeout + from core.database.database_settings import DatabaseSettings from core.database.postgres_pool import PostgresPool from core.environment import Environment @@ -26,15 +29,15 @@ class DBContext: except Exception as 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}") - 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]: logger.trace(f"select {statement} with args: {args}") try: return await self._pool.select_map(statement, args) - except Exception as e: + except (OperationalError, PoolTimeout) as e: if self._fails >= 3: logger.error(f"Database error caused by {statement}", e) uid = uuid.uuid4() @@ -50,6 +53,9 @@ class DBContext: except Exception as e: pass return [] + except Exception as e: + logger.error(f"Database error caused by {statement}", e) + raise e async def select( self, statement: str, args=None @@ -57,7 +63,7 @@ class DBContext: logger.trace(f"select {statement} with args: {args}") try: return await self._pool.select(statement, args) - except Exception as e: + except (OperationalError, PoolTimeout) as e: if self._fails >= 3: logger.error(f"Database error caused by {statement}", e) uid = uuid.uuid4() @@ -73,3 +79,6 @@ class DBContext: except Exception as e: pass return [] + except Exception as e: + logger.error(f"Database error caused by {statement}", e) + raise e diff --git a/api/src/core/database/postgres_pool.py b/api/src/core/database/postgres_pool.py index 217eabe..6115c40 100644 --- a/api/src/core/database/postgres_pool.py +++ b/api/src/core/database/postgres_pool.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Any from psycopg import sql from psycopg_pool import AsyncConnectionPool, PoolTimeout @@ -21,7 +21,7 @@ class PostgresPool: f"host={database_settings.host} " f"port={database_settings.port} " f"user={database_settings.user} " - f"password={B64Helper.decode(database_settings.password)} " + f"password={database_settings.password} " f"dbname={database_settings.database}" ) self._pool_size = pool_size @@ -41,18 +41,31 @@ class PostgresPool: logger.fatal(f"Failed to connect to the database", e) 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 similar to the execute() function in the psycopg module. :param query: SQL clause :param args: args needed by the SQL clause + :param multi: if the query is a multi-statement :return: return result """ async with await self._get_pool() as pool: async with pool.connection() as con: async with con.cursor() as cursor: - await cursor.execute(sql.SQL(query), args) + await self._exec_sql(cursor, query, args, multi) if ( cursor.description is not None @@ -68,34 +81,36 @@ class PostgresPool: else: 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 similar to the execute() function in the psycopg module. :param query: SQL clause :param args: args needed by the SQL clause + :param multi: if the query is a multi-statement :return: return result """ async with await self._get_pool() as pool: async with pool.connection() as con: 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() 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 similar to the execute() function in the psycopg module. :param query: SQL clause :param args: args needed by the SQL clause + :param multi: if the query is a multi-statement :return: return result """ async with await self._get_pool() as pool: async with pool.connection() as con: 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_map: list[dict] = [] diff --git a/api/src/data/service/migration_service.py b/api/src/data/service/migration_service.py index e336284..23293c4 100644 --- a/api/src/data/service/migration_service.py +++ b/api/src/data/service/migration_service.py @@ -77,7 +77,7 @@ class MigrationService: 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( ExecutedMigration(migration.name), skip_editor=True diff --git a/web/src/app/core/guard/permission.guard.ts b/web/src/app/core/guard/permission.guard.ts index fc07bcf..13099ed 100644 --- a/web/src/app/core/guard/permission.guard.ts +++ b/web/src/app/core/guard/permission.guard.ts @@ -4,6 +4,7 @@ import { Logger } from 'src/app/service/logger.service'; import { ToastService } from 'src/app/service/toast.service'; import { AuthService } from 'src/app/service/auth.service'; import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; +import { FeatureFlagService } from 'src/app/service/feature-flag.service'; const log = new Logger('PermissionGuard'); @@ -14,11 +15,17 @@ export class PermissionGuard { constructor( private router: Router, private toast: ToastService, - private auth: AuthService + private auth: AuthService, + private features: FeatureFlagService ) {} async canActivate(route: ActivatedRouteSnapshot): Promise { 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) { return true; diff --git a/web/src/app/modules/admin/admin.module.ts b/web/src/app/modules/admin/admin.module.ts index d47a27d..f6be88c 100644 --- a/web/src/app/modules/admin/admin.module.ts +++ b/web/src/app/modules/admin/admin.module.ts @@ -22,7 +22,7 @@ const routes: Routes = [ m => m.GroupsModule ), canActivate: [PermissionGuard], - data: { permissions: [PermissionsEnum.groups] }, + data: { permissions: [PermissionsEnum.groups], checkByPerUserSetup: true }, }, { path: 'urls', @@ -36,6 +36,7 @@ const routes: Routes = [ PermissionsEnum.shortUrls, PermissionsEnum.shortUrlsByAssignment, ], + checkByPerUserSetup: true, }, }, { diff --git a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html index 8240320..e4bddcb 100644 --- a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html +++ b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html @@ -65,7 +65,7 @@ > -
+

{{ 'common.domain' | translate }}

diff --git a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts index fa7ea84..48ebe48 100644 --- a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts +++ b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } 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'; @@ -10,29 +10,45 @@ import { import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-urls.data.service'; import { Group } from 'src/app/model/entities/group'; import { Domain } from 'src/app/model/entities/domain'; +import { FeatureFlagService } from 'src/app/service/feature-flag.service'; @Component({ selector: 'app-short-url-form-page', templateUrl: './short-url-form-page.component.html', styleUrl: './short-url-form-page.component.scss', }) -export class ShortUrlFormPageComponent extends FormPageBase< - ShortUrl, - ShortUrlCreateInput, - ShortUrlUpdateInput, - ShortUrlsDataService -> { +export class ShortUrlFormPageComponent + extends FormPageBase< + ShortUrl, + ShortUrlCreateInput, + ShortUrlUpdateInput, + ShortUrlsDataService + > + implements OnInit +{ groups: Group[] = []; domains: Domain[] = []; - constructor(private toast: ToastService) { + isPerUserSetup = true; + + constructor( + private features: FeatureFlagService, + private toast: ToastService + ) { super(); this.dataService.getAllGroups().subscribe(groups => { this.groups = groups; }); - this.dataService.getAllDomains().subscribe(domains => { - this.domains = domains; - }); + } + + async ngOnInit() { + this.isPerUserSetup = await this.features.get('PerUserSetup'); + + if (!this.isPerUserSetup) { + this.dataService.getAllDomains().subscribe(domains => { + this.domains = domains; + }); + } if (!this.nodeId) { this.node = this.new(); diff --git a/web/src/app/modules/admin/short-urls/short-urls.module.ts b/web/src/app/modules/admin/short-urls/short-urls.module.ts index 4af7061..a144ca7 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.module.ts +++ b/web/src/app/modules/admin/short-urls/short-urls.module.ts @@ -22,6 +22,7 @@ const routes: Routes = [ canActivate: [PermissionGuard], data: { permissions: [PermissionsEnum.shortUrlsCreate], + checkByPerUserSetup: true, }, }, { @@ -30,6 +31,7 @@ const routes: Routes = [ canActivate: [PermissionGuard], data: { permissions: [PermissionsEnum.shortUrlsUpdate], + checkByPerUserSetup: true, }, }, { @@ -37,7 +39,8 @@ const routes: Routes = [ component: HistoryComponent, canActivate: [PermissionGuard], data: { - permissions: [PermissionsEnum.domains], + permissions: [PermissionsEnum.shortUrls], + checkByPerUserSetup: true, }, }, ], diff --git a/web/src/app/modules/admin/short-urls/short-urls.page.ts b/web/src/app/modules/admin/short-urls/short-urls.page.ts index 460c05e..e20171a 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.page.ts +++ b/web/src/app/modules/admin/short-urls/short-urls.page.ts @@ -12,6 +12,7 @@ import QrCodeWithLogo from 'qrcode-with-logos'; import { FileUpload, FileUploadHandlerEvent } from 'primeng/fileupload'; import { ConfigService } from 'src/app/service/config.service'; import { ResolvedTableColumn } from 'src/app/modules/shared/components/table/table.model'; +import { FeatureFlagService } from 'src/app/service/feature-flag.service'; @Component({ selector: 'app-short-urls', @@ -67,7 +68,8 @@ export class ShortUrlsPage private toast: ToastService, private confirmation: ConfirmationDialogService, private auth: AuthService, - private config: ConfigService + private config: ConfigService, + private features: FeatureFlagService ) { super(true, { read: [PermissionsEnum.shortUrls], @@ -80,21 +82,26 @@ export class ShortUrlsPage async ngOnInit() { this.hasPermissions = { - read: await this.auth.hasAnyPermissionLazy( - this.requiredPermissions.read ?? [] - ), - create: await this.auth.hasAnyPermissionLazy( - this.requiredPermissions.create ?? [] - ), - update: await this.auth.hasAnyPermissionLazy( - this.requiredPermissions.update ?? [] - ), - delete: await this.auth.hasAnyPermissionLazy( - this.requiredPermissions.delete ?? [] - ), - restore: await this.auth.hasAnyPermissionLazy( - this.requiredPermissions.restore ?? [] - ), + read: + (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.read ?? [] + )) || (await this.features.get('PerUserSetup')), + create: + (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.create ?? [] + )) || (await this.features.get('PerUserSetup')), + update: + (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.update ?? [] + )) || (await this.features.get('PerUserSetup')), + delete: + (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.delete ?? [] + )) || (await this.features.get('PerUserSetup')), + restore: + (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.restore ?? [] + )) || (await this.features.get('PerUserSetup')), }; }