[WIP] User space handling
Some checks failed
Test API before pr merge / test-lint (pull_request) Failing after 9s
Test before pr merge / test-lint (pull_request) Successful in 39s
Test before pr merge / test-translation-lint (pull_request) Successful in 34s
Test before pr merge / test-before-merge (pull_request) Successful in 1m36s

This commit is contained in:
Sven Heidemann 2025-04-20 12:34:28 +02:00
parent d190d2f218
commit 0ed3cb846d
5 changed files with 61 additions and 16 deletions

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.user_setting_input import UserSettingInput from api_graphql.input.user_setting_input import UserSettingInput
from core.logger import APILogger from core.logger import APILogger
from core.string import first_to_lower
from data.schemas.public.user_setting import UserSetting from data.schemas.public.user_setting import UserSetting
from data.schemas.public.user_setting_dao import userSettingsDao from data.schemas.public.user_setting_dao import userSettingsDao
from data.schemas.system.setting_dao import settingsDao from data.schemas.system.setting_dao import settingsDao
@ -13,12 +15,19 @@ logger = APILogger(__name__)
class UserSettingMutation(MutationABC): class UserSettingMutation(MutationABC):
def __init__(self): def __init__(self):
MutationABC.__init__(self, "UserSetting") MutationABC.__init__(self, "UserSetting")
self.mutation( self.field(
"change", MutationFieldBuilder("change")
self.resolve_change, .with_resolver(self.resolve_change)
UserSettingInput, .with_change_broadcast(
require_any_permission=[Permissions.settings_update], f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
) )
.with_input(UserSettingInput, "input")
.with_require_any([], [self._x])
)
@staticmethod
async def _x(ctx):
return ctx.data.user_id == (await Route.get_user()).id
@staticmethod @staticmethod
async def resolve_change(obj: UserSettingInput, *_): async def resolve_change(obj: UserSettingInput, *_):

View File

@ -1,3 +1,5 @@
from typing import Union
from core.configuration.feature_flags_enum import FeatureFlagsEnum from core.configuration.feature_flags_enum import FeatureFlagsEnum
from core.environment import Environment from core.environment import Environment
from data.schemas.system.feature_flag_dao import featureFlagDao from data.schemas.system.feature_flag_dao import featureFlagDao
@ -6,19 +8,31 @@ from data.schemas.system.feature_flag_dao import featureFlagDao
class FeatureFlags: class FeatureFlags:
_flags = { _flags = {
FeatureFlagsEnum.version_endpoint.value: True, # 15.01.2025 FeatureFlagsEnum.version_endpoint.value: True, # 15.01.2025
FeatureFlagsEnum.technical_demo_banner.value: False, # 18.04.2025
FeatureFlagsEnum.per_user_setup.value: Environment.get( FeatureFlagsEnum.per_user_setup.value: Environment.get(
"PER_USER_SETUP", bool, False "PER_USER_SETUP", bool, False
), # 18.04.2025 ), # 18.04.2025
} }
_overwrite_flags = [
FeatureFlagsEnum.per_user_setup.value,
]
@staticmethod
def overwrite_flag(key: str):
return key in FeatureFlags._overwrite_flags
@staticmethod @staticmethod
def get_default(key: FeatureFlagsEnum) -> bool: def get_default(key: FeatureFlagsEnum) -> bool:
return FeatureFlags._flags[key.value] return FeatureFlags._flags[key.value]
@staticmethod @staticmethod
async def has_feature(key: FeatureFlagsEnum) -> bool: async def has_feature(key: Union[str, FeatureFlagsEnum]) -> bool:
value = await featureFlagDao.find_by_key(key.value) key_value = key.value if isinstance(key, FeatureFlagsEnum) else key
if value is None:
return FeatureFlags.get_default(key)
return value.value value = await featureFlagDao.find_by_key(key_value)
return (
value.value
if value
else FeatureFlags.get_default(FeatureFlagsEnum(key_value))
)

View File

@ -3,4 +3,5 @@ from enum import Enum
class FeatureFlagsEnum(Enum): class FeatureFlagsEnum(Enum):
version_endpoint = "VersionEndpoint" version_endpoint = "VersionEndpoint"
technical_demo_banner = "TechnicalDemoBanner"
per_user_setup = "PerUserSetup" per_user_setup = "PerUserSetup"

View File

@ -21,6 +21,7 @@ class FeatureFlagsSeeder(DataSeederABC):
x.value: FeatureFlags.get_default(x) for x in FeatureFlagsEnum x.value: FeatureFlags.get_default(x) for x in FeatureFlagsEnum
} }
# Create new feature flags
to_create = [ to_create = [
FeatureFlag(0, x, possible_feature_flags[x]) FeatureFlag(0, x, possible_feature_flags[x])
for x in possible_feature_flags.keys() for x in possible_feature_flags.keys()
@ -31,6 +32,19 @@ class FeatureFlagsSeeder(DataSeederABC):
to_create_dicts = {x.key: x.value for x in to_create} to_create_dicts = {x.key: x.value for x in to_create}
logger.debug(f"Created feature flags: {to_create_dicts}") logger.debug(f"Created feature flags: {to_create_dicts}")
# Update existing feature flags if they can be overwritten and have a different value
to_update = [
FeatureFlag(x.id, x.key, possible_feature_flags[x.key])
for x in feature_flags
if FeatureFlags.overwrite_flag(x.key)
and x.value != possible_feature_flags[x.key]
]
if len(to_update) > 0:
await featureFlagDao.update_many(to_update)
to_update_dicts = {x.key: x.value for x in to_update}
logger.debug(f"Updated feature flags: {to_update_dicts}")
# Delete feature flags that are no longer defined
to_delete = [ to_delete = [
x for x in feature_flags if x.key not in possible_feature_flags.keys() x for x in feature_flags if x.key not in possible_feature_flags.keys()
] ]

View File

@ -3,6 +3,7 @@ import { BehaviorSubject } from 'rxjs';
import { MenuElement } from 'src/app/model/view/menu-element'; import { MenuElement } from 'src/app/model/view/menu-element';
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';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -11,7 +12,10 @@ export class SidebarService {
visible$ = new BehaviorSubject<boolean>(true); visible$ = new BehaviorSubject<boolean>(true);
elements$ = new BehaviorSubject<MenuElement[]>([]); elements$ = new BehaviorSubject<MenuElement[]>([]);
constructor(private auth: AuthService) { constructor(
private auth: AuthService,
private featureFlags: FeatureFlagService
) {
this.auth.user$.subscribe(async () => { this.auth.user$.subscribe(async () => {
await this.setElements(); await this.setElements();
}); });
@ -40,16 +44,19 @@ export class SidebarService {
label: 'common.groups', label: 'common.groups',
icon: 'pi pi-tags', icon: 'pi pi-tags',
routerLink: ['/admin/groups'], routerLink: ['/admin/groups'],
visible: await this.auth.hasAnyPermissionLazy([PermissionsEnum.groups]), visible:
(await this.auth.hasAnyPermissionLazy([PermissionsEnum.groups])) ||
(await this.featureFlags.get('PerUserSetup')),
}, },
{ {
label: 'common.urls', label: 'common.urls',
icon: 'pi pi-tag', icon: 'pi pi-tag',
routerLink: ['/admin/urls'], routerLink: ['/admin/urls'],
visible: await this.auth.hasAnyPermissionLazy([ visible:
(await this.auth.hasAnyPermissionLazy([
PermissionsEnum.shortUrls, PermissionsEnum.shortUrls,
PermissionsEnum.shortUrlsByAssignment, PermissionsEnum.shortUrlsByAssignment,
]), ])) || (await this.featureFlags.get('PerUserSetup')),
}, },
await this.sectionAdmin(), await this.sectionAdmin(),
]; ];