[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

This commit is contained in:
Sven Heidemann 2025-04-21 18:12:21 +02:00
parent 0ed3cb846d
commit e378393813
19 changed files with 214 additions and 134 deletions

View File

@ -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],

View File

@ -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,14 +87,16 @@ 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(
self, self,

View File

@ -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_update, Permissions.short_urls_create,
Permissions.short_urls_delete, Permissions.short_urls_update,
], Permissions.short_urls_delete,
],
[by_user_setup_mutation],
),
) )
self.add_mutation_type( self.add_mutation_type(

View File

@ -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

View File

@ -131,11 +131,11 @@ 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( self.field(group_field)
group_field
)
short_url_field = ( short_url_field = (
DaoFieldBuilder("shortUrls") DaoFieldBuilder("shortUrls")
@ -149,11 +149,11 @@ 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( self.field(short_url_field)
short_url_field
)
self.field( self.field(
ResolverFieldBuilder("settings") ResolverFieldBuilder("settings")

View File

@ -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)

View File

@ -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,9 +138,23 @@ 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):
f"LEFT JOIN {table_name} ON {table_name}.{primary_attr} = {self._table_name}.{foreign_attr}" 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 self.__foreign_tables[attr] = table_name
def use_external_fields(self, builder: ExternalDataTempTableBuilder): 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: 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):

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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] = []

View File

@ -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

View File

@ -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;

View File

@ -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,
}, },
}, },
{ {

View File

@ -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">

View File

@ -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
ShortUrl, extends FormPageBase<
ShortUrlCreateInput, ShortUrl,
ShortUrlUpdateInput, ShortUrlCreateInput,
ShortUrlsDataService ShortUrlUpdateInput,
> { 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;
}); });
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) { if (!this.nodeId) {
this.node = this.new(); this.node = this.new();

View File

@ -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,
}, },
}, },
], ],

View File

@ -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:
this.requiredPermissions.read ?? [] (await this.auth.hasAnyPermissionLazy(
), this.requiredPermissions.read ?? []
create: await this.auth.hasAnyPermissionLazy( )) || (await this.features.get('PerUserSetup')),
this.requiredPermissions.create ?? [] create:
), (await this.auth.hasAnyPermissionLazy(
update: await this.auth.hasAnyPermissionLazy( this.requiredPermissions.create ?? []
this.requiredPermissions.update ?? [] )) || (await this.features.get('PerUserSetup')),
), update:
delete: await this.auth.hasAnyPermissionLazy( (await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.delete ?? [] this.requiredPermissions.update ?? []
), )) || (await this.features.get('PerUserSetup')),
restore: await this.auth.hasAnyPermissionLazy( delete:
this.requiredPermissions.restore ?? [] (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')),
}; };
} }