diff --git a/api/src/api_graphql/abc/query_abc.py b/api/src/api_graphql/abc/query_abc.py index 0ea4d6b..c3d2ca6 100644 --- a/api/src/api_graphql/abc/query_abc.py +++ b/api/src/api_graphql/abc/query_abc.py @@ -213,7 +213,6 @@ class QueryABC(ObjectType): f"{field.name}: {field.input_type.__name__} {kwargs[field.input_key]}" ) input_obj = field.input_type(kwargs[field.input_key]) - del kwargs[field.input_key] return await resolver_wrapper(input_obj, mutation, info, **kwargs) diff --git a/api/src/api_graphql/field/mutation_field_builder.py b/api/src/api_graphql/field/mutation_field_builder.py index bf33dda..b6b7576 100644 --- a/api/src/api_graphql/field/mutation_field_builder.py +++ b/api/src/api_graphql/field/mutation_field_builder.py @@ -58,7 +58,7 @@ class MutationFieldBuilder(FieldBuilderABC): self._resolver = resolver_wrapper return self - def with_input(self, input_type: Type[InputABC], input_key: str = None) -> Self: + def with_input(self, input_type: Type[InputABC], input_key: str = "input") -> Self: self._input_type = input_type self._input_key = input_key return self diff --git a/api/src/api_graphql/mutation.py b/api/src/api_graphql/mutation.py index 59b2a5c..2514018 100644 --- a/api/src/api_graphql/mutation.py +++ b/api/src/api_graphql/mutation.py @@ -46,11 +46,14 @@ class Mutation(MutationABC): self.add_mutation_type( "group", "Group", - require_any_permission=[ - Permissions.groups_create, - Permissions.groups_update, - Permissions.groups_delete, - ], + require_any=( + [ + Permissions.groups_create, + Permissions.groups_update, + Permissions.groups_delete, + ], + [by_user_setup_mutation], + ), ) self.add_mutation_type( "shortUrl", diff --git a/api/src/api_graphql/mutations/group_mutation.py b/api/src/api_graphql/mutations/group_mutation.py index 6f0b1dd..16addbd 100644 --- a/api/src/api_graphql/mutations/group_mutation.py +++ b/api/src/api_graphql/mutations/group_mutation.py @@ -2,8 +2,10 @@ from typing import Optional 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.group_create_input import GroupCreateInput from api_graphql.input.group_update_input import GroupUpdateInput +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,27 +22,30 @@ class GroupMutation(MutationABC): def __init__(self): MutationABC.__init__(self, "Group") - self.mutation( - "create", - self.resolve_create, - GroupCreateInput, - require_any_permission=[Permissions.groups_create], + self.field( + MutationFieldBuilder("create") + .with_resolver(self.resolve_create) + .with_input(GroupCreateInput) + .with_require_any([Permissions.groups_create], [by_user_setup_mutation]) ) - self.mutation( - "update", - self.resolve_update, - GroupUpdateInput, - require_any_permission=[Permissions.groups_update], + + self.field( + MutationFieldBuilder("update") + .with_resolver(self.resolve_update) + .with_input(GroupUpdateInput) + .with_require_any([Permissions.groups_update], [by_user_setup_mutation]) ) - self.mutation( - "delete", - self.resolve_delete, - require_any_permission=[Permissions.groups_delete], + + self.field( + MutationFieldBuilder("delete") + .with_resolver(self.resolve_delete) + .with_require_any([Permissions.groups_delete], [by_user_setup_mutation]) ) - self.mutation( - "restore", - self.resolve_restore, - require_any_permission=[Permissions.groups_delete], + + self.field( + MutationFieldBuilder("restore") + .with_resolver(self.resolve_restore) + .with_require_any([Permissions.groups_delete], [by_user_setup_mutation]) ) @staticmethod @@ -75,6 +80,10 @@ class GroupMutation(MutationABC): async def resolve_create(cls, obj: GroupCreateInput, *_): logger.debug(f"create group: {obj.__dict__}") + already_exists = await groupDao.find_by({Group.name: obj.name}) + if len(already_exists) > 0: + raise ValueError(f"Group {obj.name} already exists") + group = Group( 0, obj.name, @@ -98,6 +107,12 @@ class GroupMutation(MutationABC): raise ValueError(f"Group with id {obj.id} not found") if obj.name is not None: + already_exists = await groupDao.find_by( + {Group.name: obj.name, Group.id: {"ne": obj.id}} + ) + if len(already_exists) > 0: + raise ValueError(f"Group {obj.name} already exists") + group = await groupDao.get_by_id(obj.id) group.name = obj.name await groupDao.update(group) diff --git a/api/src/api_graphql/mutations/short_url_mutation.py b/api/src/api_graphql/mutations/short_url_mutation.py index f39cefa..b471fba 100644 --- a/api/src/api_graphql/mutations/short_url_mutation.py +++ b/api/src/api_graphql/mutations/short_url_mutation.py @@ -58,6 +58,10 @@ class ShortUrlMutation(MutationABC): async def resolve_create(obj: ShortUrlCreateInput, *_): logger.debug(f"create short_url: {obj.__dict__}") + already_exists = await shortUrlDao.find_by({ShortUrl.short_url: obj.short_url}) + if len(already_exists) > 0: + raise ValueError(f"Short URL {obj.short_url} already exists") + short_url = ShortUrl( 0, obj.short_url, @@ -82,6 +86,11 @@ class ShortUrlMutation(MutationABC): short_url = await shortUrlDao.get_by_id(obj.id) if obj.short_url is not None: + already_exists = await shortUrlDao.find_by( + {ShortUrl.short_url: obj.short_url} + ) + if len(already_exists) > 0: + raise ValueError(f"Short URL {obj.short_url} already exists") short_url.short_url = obj.short_url if obj.target_url is not None: diff --git a/api/src/api_graphql/require_any_resolvers.py b/api/src/api_graphql/require_any_resolvers.py index e688fd9..8eea17f 100644 --- a/api/src/api_graphql/require_any_resolvers.py +++ b/api/src/api_graphql/require_any_resolvers.py @@ -36,11 +36,8 @@ async def by_user_setup_resolver(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) + return all(x.user_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) + return await FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup) diff --git a/api/src/api_graphql/service/query_context.py b/api/src/api_graphql/service/query_context.py index 40f85da..003d73b 100644 --- a/api/src/api_graphql/service/query_context.py +++ b/api/src/api_graphql/service/query_context.py @@ -15,6 +15,7 @@ class QueryContext: data: Any, user: Optional[User], user_permissions: Optional[list[Permissions]], + is_mutation: bool = False, *args, **kwargs ): @@ -31,11 +32,17 @@ class QueryContext: self._resolve_info = arg continue - self._filter = kwargs.get("filter", {}) + self._filter = kwargs.get("filters", {}) self._sort = kwargs.get("sort", {}) self._skip = get_value(kwargs, "skip", int) self._take = get_value(kwargs, "take", int) + self._input = kwargs.get("input", None) + self._args = args + self._kwargs = kwargs + + self._is_mutation = is_mutation + @property def data(self): return self._data @@ -64,5 +71,21 @@ class QueryContext: def take(self) -> Optional[int]: return self._take + @property + def input(self) -> Optional[Any]: + return self._input + + @property + def args(self) -> tuple: + return self._args + + @property + def kwargs(self) -> dict: + return self._kwargs + + @property + def is_mutation(self) -> bool: + return self._is_mutation + def has_permission(self, permission: Permissions) -> bool: return permission.value in self._user_permissions diff --git a/web/src/app/modules/admin/domains/domains.data.service.ts b/web/src/app/modules/admin/domains/domains.data.service.ts index 162f453..2af9aca 100644 --- a/web/src/app/modules/admin/domains/domains.data.service.ts +++ b/web/src/app/modules/admin/domains/domains.data.service.ts @@ -228,7 +228,7 @@ export class DomainsDataService return this.apollo .mutate<{ domain: { delete: boolean } }>({ mutation: gql` - mutation deleteDomain($id: ID!) { + mutation deleteDomain($id: Int!) { domain { delete(id: $id) } @@ -251,7 +251,7 @@ export class DomainsDataService return this.apollo .mutate<{ domain: { restore: boolean } }>({ mutation: gql` - mutation restoreDomain($id: ID!) { + mutation restoreDomain($id: Int!) { domain { restore(id: $id) } diff --git a/web/src/app/modules/admin/groups/form-page/group-form-page.component.html b/web/src/app/modules/admin/groups/form-page/group-form-page.component.html index 150c081..8f0373c 100644 --- a/web/src/app/modules/admin/groups/form-page/group-form-page.component.html +++ b/web/src/app/modules/admin/groups/form-page/group-form-page.component.html @@ -27,8 +27,9 @@ type="text" formControlName="name"/> -
+
{ +export class GroupFormPageComponent + extends FormPageBase< + Group, + GroupCreateInput, + GroupUpdateInput, + GroupsDataService + > + implements OnInit +{ roles: Role[] = []; + isPerUserSetup = true; constructor( + private features: FeatureFlagService, private toast: ToastService, private cds: CommonDataService ) { super(); - this.cds.getAllRoles().subscribe(roles => { - this.roles = roles; - }); + } + + async ngOnInit() { + this.isPerUserSetup = await this.features.get('PerUserSetup'); + if (!this.isPerUserSetup) { + this.cds.getAllRoles().subscribe(roles => { + this.roles = roles; + }); + } if (!this.nodeId) { this.node = this.new(); diff --git a/web/src/app/modules/admin/groups/groups.data.service.ts b/web/src/app/modules/admin/groups/groups.data.service.ts index ba84fa3..c40ce19 100644 --- a/web/src/app/modules/admin/groups/groups.data.service.ts +++ b/web/src/app/modules/admin/groups/groups.data.service.ts @@ -238,7 +238,7 @@ export class GroupsDataService return this.apollo .mutate<{ group: { delete: boolean } }>({ mutation: gql` - mutation deleteGroup($id: ID!) { + mutation deleteGroup($id: Int!) { group { delete(id: $id) } @@ -261,7 +261,7 @@ export class GroupsDataService return this.apollo .mutate<{ group: { restore: boolean } }>({ mutation: gql` - mutation restoreGroup($id: ID!) { + mutation restoreGroup($id: Int!) { group { restore(id: $id) } diff --git a/web/src/app/modules/admin/groups/groups.module.ts b/web/src/app/modules/admin/groups/groups.module.ts index 813c76f..f8762ad 100644 --- a/web/src/app/modules/admin/groups/groups.module.ts +++ b/web/src/app/modules/admin/groups/groups.module.ts @@ -21,7 +21,8 @@ const routes: Routes = [ component: GroupFormPageComponent, canActivate: [PermissionGuard], data: { - permissions: [PermissionsEnum.apiKeysCreate], + permissions: [PermissionsEnum.groupsCreate], + checkByPerUserSetup: true, }, }, { @@ -29,7 +30,8 @@ const routes: Routes = [ component: GroupFormPageComponent, canActivate: [PermissionGuard], data: { - permissions: [PermissionsEnum.apiKeysUpdate], + permissions: [PermissionsEnum.groupsUpdate], + checkByPerUserSetup: true, }, }, { @@ -37,7 +39,8 @@ const routes: Routes = [ component: HistoryComponent, canActivate: [PermissionGuard], data: { - permissions: [PermissionsEnum.domains], + permissions: [PermissionsEnum.groups], + checkByPerUserSetup: true, }, }, ], diff --git a/web/src/app/modules/admin/groups/groups.page.ts b/web/src/app/modules/admin/groups/groups.page.ts index e1447e7..b080d40 100644 --- a/web/src/app/modules/admin/groups/groups.page.ts +++ b/web/src/app/modules/admin/groups/groups.page.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { PageBase } from 'src/app/core/base/page-base'; import { ToastService } from 'src/app/service/toast.service'; import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service'; @@ -6,28 +6,63 @@ import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { Group } from 'src/app/model/entities/group'; import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service'; import { GroupsColumns } from 'src/app/modules/admin/groups/groups.columns'; +import { AuthService } from 'src/app/service/auth.service'; +import { ConfigService } from 'src/app/service/config.service'; +import { FeatureFlagService } from 'src/app/service/feature-flag.service'; @Component({ selector: 'app-groups', templateUrl: './groups.page.html', styleUrl: './groups.page.scss', }) -export class GroupsPage extends PageBase< - Group, - GroupsDataService, - GroupsColumns -> { +export class GroupsPage + extends PageBase + implements OnInit +{ constructor( private toast: ToastService, - private confirmation: ConfirmationDialogService + private confirmation: ConfirmationDialogService, + private auth: AuthService, + private config: ConfigService, + private features: FeatureFlagService ) { - super(true, { - read: [PermissionsEnum.groups], - create: [PermissionsEnum.groupsCreate], - update: [PermissionsEnum.groupsUpdate], - delete: [PermissionsEnum.groupsDelete], - restore: [PermissionsEnum.groupsDelete], - }); + super(true); + } + + async ngOnInit() { + this.requiredPermissions = { + read: (await this.features.get('PerUserSetup')) + ? [] + : [PermissionsEnum.groups], + create: (await this.features.get('PerUserSetup')) + ? [] + : (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.create ?? [] + )) + ? (this.requiredPermissions.create ?? []) + : [], + update: (await this.features.get('PerUserSetup')) + ? [] + : (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.update ?? [] + )) + ? (this.requiredPermissions.update ?? []) + : [], + delete: (await this.features.get('PerUserSetup')) + ? [] + : (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.delete ?? [] + )) + ? (this.requiredPermissions.delete ?? []) + : [], + restore: (await this.features.get('PerUserSetup')) + ? [] + : (await this.auth.hasAnyPermissionLazy( + this.requiredPermissions.restore ?? [] + )) + ? (this.requiredPermissions.restore ?? []) + : [], + }; } load(silent?: boolean): void { diff --git a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts index d3d754b..ddfe661 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts +++ b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts @@ -67,9 +67,13 @@ export class ShortUrlsDataService id name } + + ...DB_MODEL } } } + + ${DB_MODEL_FRAGMENT} `, variables: { filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])], @@ -98,9 +102,13 @@ export class ShortUrlsDataService id name } + + ...DB_MODEL } } } + + ${DB_MODEL_FRAGMENT} `, variables: { filter: [{ group: { isNull: true } }, ...(filter ?? [])], @@ -315,7 +323,7 @@ export class ShortUrlsDataService return this.apollo .mutate<{ shortUrl: { delete: boolean } }>({ mutation: gql` - mutation deleteShortUrl($id: ID!) { + mutation deleteShortUrl($id: Int!) { shortUrl { delete(id: $id) } @@ -338,7 +346,7 @@ export class ShortUrlsDataService return this.apollo .mutate<{ shortUrl: { restore: boolean } }>({ mutation: gql` - mutation restoreShortUrl($id: ID!) { + mutation restoreShortUrl($id: Int!) { shortUrl { restore(id: $id) } diff --git a/web/src/app/modules/admin/short-urls/short-urls.page.html b/web/src/app/modules/admin/short-urls/short-urls.page.html index 1ead5a3..e0c3333 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.page.html +++ b/web/src/app/modules/admin/short-urls/short-urls.page.html @@ -41,7 +41,7 @@ icon="pi pi-pencil" tooltipPosition="left" pTooltip="{{ 'table.update' | translate }}" - [disabled]="url?.deleted" + [disabled]="url.deleted" routerLink="edit/{{ url.id }}">