From 667554fd8ed35b86efe1b0aa001638d729545ee5 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Sat, 20 Dec 2025 21:17:42 +0300 Subject: [PATCH] Add resource permission management component Introduces ResourcePermissionManagementComponent with UI and logic for managing resource-based permissions. Updates models and service to support resource permission APIs, and exports the new component in the index. --- .../proxy/src/lib/proxy/models.ts | 63 ++++ .../src/lib/proxy/permissions.service.ts | 88 ++++- .../src/lib/components/index.ts | 1 + ...ource-permission-management.component.html | 174 ++++++++++ ...esource-permission-management.component.ts | 325 ++++++++++++++++++ 5 files changed, 644 insertions(+), 7 deletions(-) create mode 100644 npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.html create mode 100644 npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.ts diff --git a/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/models.ts b/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/models.ts index cbba2214ec..49e67f7614 100644 --- a/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/models.ts +++ b/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/models.ts @@ -4,6 +4,27 @@ export interface GetPermissionListResultDto { groups: PermissionGroupDto[]; } +export interface GetResourcePermissionDefinitionListResultDto { + permissions?: ResourcePermissionDefinitionDto[]; +} + +export interface GetResourcePermissionListResultDto { + permissions?: ResourcePermissionGrantInfoDto[]; +} + +export interface GetResourcePermissionWithProviderListResultDto { + permissions?: ResourcePermissionWithProdiverGrantInfoDto[]; +} + +export interface GetResourceProviderListResultDto { + providers?: ResourceProviderDto[]; +} + +export interface GrantedResourcePermissionDto { + name?: string; + displayName?: string; +} + export interface PermissionGrantInfoDto { name?: string; displayName?: string; @@ -17,6 +38,8 @@ export interface PermissionGroupDto { name?: string; displayName?: string; permissions: PermissionGrantInfoDto[]; + displayNameKey?: string; + displayNameResource?: string; } export interface ProviderInfoDto { @@ -24,6 +47,40 @@ export interface ProviderInfoDto { providerKey?: string; } +export interface ResourcePermissionDefinitionDto { + name?: string; + displayName?: string; +} + +export interface ResourcePermissionGrantInfoDto { + providerName?: string; + providerKey?: string; + providerDisplayName?: string; + providerNameDisplayName?: string; + permissions?: GrantedResourcePermissionDto[]; +} + +export interface ResourcePermissionWithProdiverGrantInfoDto { + name?: string; + displayName?: string; + providers?: string[]; + isGranted?: boolean; +} + +export interface ResourceProviderDto { + name?: string; + displayName?: string; +} + +export interface SearchProviderKeyInfo { + providerKey?: string; + providerDisplayName?: string; +} + +export interface SearchProviderKeyListResultDto { + keys?: SearchProviderKeyInfo[]; +} + export interface UpdatePermissionDto { name?: string; isGranted: boolean; @@ -32,3 +89,9 @@ export interface UpdatePermissionDto { export interface UpdatePermissionsDto { permissions: UpdatePermissionDto[]; } + +export interface UpdateResourcePermissionsDto { + providerName?: string; + providerKey?: string; + permissions?: string[]; +} diff --git a/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts b/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts index dbd13ca778..8f28edf3fb 100644 --- a/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts +++ b/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts @@ -1,5 +1,5 @@ -import type { GetPermissionListResultDto, UpdatePermissionsDto } from './models'; -import { RestService } from '@abp/ng.core'; +import type { GetPermissionListResultDto, GetResourcePermissionDefinitionListResultDto, GetResourcePermissionListResultDto, GetResourcePermissionWithProviderListResultDto, GetResourceProviderListResultDto, SearchProviderKeyListResultDto, UpdatePermissionsDto, UpdateResourcePermissionsDto } from './models'; +import { RestService, Rest } from '@abp/ng.core'; import { Injectable, inject } from '@angular/core'; @Injectable({ @@ -7,23 +7,97 @@ import { Injectable, inject } from '@angular/core'; }) export class PermissionsService { private restService = inject(RestService); - apiName = 'AbpPermissionManagement'; - get = (providerName: string, providerKey: string) => + + deleteResource = (resourceName: string, resourceKey: string, providerName: string, providerKey: string, config?: Partial) => + this.restService.request({ + method: 'DELETE', + url: '/api/permission-management/permissions/resource', + params: { resourceName, resourceKey, providerName, providerKey }, + }, + { apiName: this.apiName, ...config }); + + + get = (providerName: string, providerKey: string, config?: Partial) => this.restService.request({ method: 'GET', url: '/api/permission-management/permissions', params: { providerName, providerKey }, }, - { apiName: this.apiName }); + { apiName: this.apiName, ...config }); + + + getByGroup = (groupName: string, providerName: string, providerKey: string, config?: Partial) => + this.restService.request({ + method: 'GET', + url: '/api/permission-management/permissions/by-group', + params: { groupName, providerName, providerKey }, + }, + { apiName: this.apiName, ...config }); + + + getResource = (resourceName: string, resourceKey: string, config?: Partial) => + this.restService.request({ + method: 'GET', + url: '/api/permission-management/permissions/resource', + params: { resourceName, resourceKey }, + }, + { apiName: this.apiName, ...config }); + + + getResourceByProvider = (resourceName: string, resourceKey: string, providerName: string, providerKey: string, config?: Partial) => + this.restService.request({ + method: 'GET', + url: '/api/permission-management/permissions/resource/by-provider', + params: { resourceName, resourceKey, providerName, providerKey }, + }, + { apiName: this.apiName, ...config }); - update = (providerName: string, providerKey: string, input: UpdatePermissionsDto) => + + getResourceDefinitions = (resourceName: string, config?: Partial) => + this.restService.request({ + method: 'GET', + url: '/api/permission-management/permissions/resource-definitions', + params: { resourceName }, + }, + { apiName: this.apiName, ...config }); + + + getResourceProviderKeyLookupServices = (resourceName: string, config?: Partial) => + this.restService.request({ + method: 'GET', + url: '/api/permission-management/permissions/resource-provider-key-lookup-services', + params: { resourceName }, + }, + { apiName: this.apiName, ...config }); + + + searchResourceProviderKey = (resourceName: string, serviceName: string, filter: string, page: number, config?: Partial) => + this.restService.request({ + method: 'GET', + url: '/api/permission-management/permissions/search-resource-provider-keys', + params: { resourceName, serviceName, filter, page }, + }, + { apiName: this.apiName, ...config }); + + + update = (providerName: string, providerKey: string, input: UpdatePermissionsDto, config?: Partial) => this.restService.request({ method: 'PUT', url: '/api/permission-management/permissions', params: { providerName, providerKey }, body: input, }, - { apiName: this.apiName }); + { apiName: this.apiName, ...config }); + + + updateResource = (resourceName: string, resourceKey: string, input: UpdateResourcePermissionsDto, config?: Partial) => + this.restService.request({ + method: 'PUT', + url: '/api/permission-management/permissions/resource', + params: { resourceName, resourceKey }, + body: input, + }, + { apiName: this.apiName, ...config }); } diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/index.ts b/npm/ng-packs/packages/permission-management/src/lib/components/index.ts index efa91b45a2..83dc62ea41 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/index.ts +++ b/npm/ng-packs/packages/permission-management/src/lib/components/index.ts @@ -1 +1,2 @@ export * from './permission-management.component'; +export * from './resource-permission-management.component'; diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.html new file mode 100644 index 0000000000..7d6f9d6016 --- /dev/null +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.html @@ -0,0 +1,174 @@ + + +

+ {{ 'AbpPermissionManagement::ResourcePermissions' | abpLocalization }} + @if (resourceDisplayName) { + - {{ resourceDisplayName }} + } +

+
+ + + @if (!hasResourcePermission() || !hasProviderKeyLookupService()) { + + } @else { + + @if (viewMode() === 'list') { +
+ +
+ + @if (resourcePermissions().length === 0) { +
+ {{ 'AbpPermissionManagement::NoPermissionsAssigned' | abpLocalization }} +
+ } @else { + + + + + + + + + + + @for (grant of resourcePermissions(); track grant.providerKey) { + + + + + + + } + +
{{ 'AbpPermissionManagement::Provider' | abpLocalization }}{{ 'AbpPermissionManagement::ProviderKey' | abpLocalization }}{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}{{ 'AbpPermissionManagement::Actions' | abpLocalization }}
{{ grant.providerNameDisplayName }}{{ grant.providerDisplayName || grant.providerKey }} + @for (perm of grant.permissions; track perm.name) { + {{ perm.displayName }} + } + + + +
+ } + } + + + @if (viewMode() === 'add') { +
+
+ @for (provider of providers(); track provider.name; let i = $index) { +
+ + +
+ } +
+ +
+ + @if (searchResults().length > 0) { +
+ @for (result of searchResults(); track result.providerKey) { + + } +
+ } +
+
+ +
+
{{ 'AbpPermissionManagement::ResourcePermissionPermissions' | abpLocalization }}
+
+ + +
+ @for (perm of permissionDefinitions(); track perm.name) { +
+ + +
+ } +
+ } + + + @if (viewMode() === 'edit') { +
+
{{ 'AbpPermissionManagement::ResourcePermissionPermissions' | abpLocalization }}
+

+ {{ editProviderName() }}: {{ editProviderKey() }} +

+
+ + +
+ @for (perm of permissionsWithProvider(); track perm.name) { +
+ + +
+ } +
+ } + } +
+ + + @if (viewMode() === 'list') { + + } @else { + + @if (viewMode() === 'add') { + + {{ 'AbpUi::Save' | abpLocalization }} + + } @else { + + {{ 'AbpUi::Save' | abpLocalization }} + + } + } + +
\ No newline at end of file diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.ts b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.ts new file mode 100644 index 0000000000..59c79cd77f --- /dev/null +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.ts @@ -0,0 +1,325 @@ +import { LocalizationPipe } from '@abp/ng.core'; +import { + ButtonComponent, + ModalCloseDirective, + ModalComponent, + ToasterService, +} from '@abp/ng.theme.shared'; +import { + GetResourcePermissionListResultDto, + GetResourceProviderListResultDto, + GetResourcePermissionDefinitionListResultDto, + GetResourcePermissionWithProviderListResultDto, + PermissionsService, + ResourcePermissionGrantInfoDto, + ResourceProviderDto, + SearchProviderKeyInfo, + ResourcePermissionDefinitionDto, + ResourcePermissionWithProdiverGrantInfoDto, +} from '@abp/ng.permission-management/proxy'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + inject, + Input, + Output, + signal, + computed, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { finalize, switchMap, of, debounceTime, Subject, distinctUntilChanged } from 'rxjs'; + +type ViewMode = 'list' | 'add' | 'edit'; + +@Component({ + selector: 'abp-resource-permission-management', + templateUrl: './resource-permission-management.component.html', + exportAs: 'abpResourcePermissionManagement', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + FormsModule, + ModalComponent, + LocalizationPipe, + ButtonComponent, + ModalCloseDirective, + ], +}) +export class ResourcePermissionManagementComponent { + protected readonly service = inject(PermissionsService); + protected readonly toasterService = inject(ToasterService); + + @Input() resourceName!: string; + @Input() resourceKey!: string; + @Input() resourceDisplayName?: string; + + protected _visible = false; + + @Input() + get visible(): boolean { + return this._visible; + } + + set visible(value: boolean) { + if (value === this._visible) return; + + if (value) { + this.openModal(); + } else { + this.resetState(); + } + this._visible = value; + this.visibleChange.emit(value); + } + + @Output() readonly visibleChange = new EventEmitter(); + + // State signals + viewMode = signal('list'); + modalBusy = signal(false); + hasResourcePermission = signal(false); + hasProviderKeyLookupService = signal(false); + + // Data + resourcePermissions = signal([]); + providers = signal([]); + permissionDefinitions = signal([]); + searchResults = signal([]); + permissionsWithProvider = signal([]); + + // Form state for add/edit + selectedProviderName = signal(''); + selectedProviderKey = signal(''); + searchFilter = signal(''); + selectedPermissions = signal([]); + + // Edit mode state + editProviderName = signal(''); + editProviderKey = signal(''); + + // Search subject for debounce + private searchSubject = new Subject(); + + constructor() { + this.searchSubject.pipe( + debounceTime(300), + distinctUntilChanged() + ).subscribe(filter => { + this.performSearch(filter); + }); + } + + openModal() { + this.modalBusy.set(true); + + // Load resource permissions and providers + this.service.getResource(this.resourceName, this.resourceKey).pipe( + switchMap(permRes => { + this.resourcePermissions.set(permRes.permissions || []); + return this.service.getResourceProviderKeyLookupServices(this.resourceName); + }), + switchMap(providerRes => { + this.providers.set(providerRes.providers || []); + this.hasProviderKeyLookupService.set((providerRes.providers?.length || 0) > 0); + if (providerRes.providers?.length) { + this.selectedProviderName.set(providerRes.providers[0].name || ''); + } + return this.service.getResourceDefinitions(this.resourceName); + }), + finalize(() => this.modalBusy.set(false)) + ).subscribe({ + next: defRes => { + this.permissionDefinitions.set(defRes.permissions || []); + this.hasResourcePermission.set((defRes.permissions?.length || 0) > 0); + }, + error: () => { + this.toasterService.error('AbpPermissionManagement::ErrorLoadingPermissions'); + } + }); + } + + resetState() { + this.viewMode.set('list'); + this.resourcePermissions.set([]); + this.selectedProviderName.set(''); + this.selectedProviderKey.set(''); + this.searchFilter.set(''); + this.selectedPermissions.set([]); + this.searchResults.set([]); + } + + // View mode navigation + goToAddMode() { + this.viewMode.set('add'); + this.selectedPermissions.set([]); + this.selectedProviderKey.set(''); + this.searchResults.set([]); + } + + goToEditMode(grant: ResourcePermissionGrantInfoDto) { + this.editProviderName.set(grant.providerName || ''); + this.editProviderKey.set(grant.providerKey || ''); + this.modalBusy.set(true); + + this.service.getResourceByProvider( + this.resourceName, + this.resourceKey, + grant.providerName || '', + grant.providerKey || '' + ).pipe( + finalize(() => this.modalBusy.set(false)) + ).subscribe({ + next: res => { + this.permissionsWithProvider.set(res.permissions || []); + this.selectedPermissions.set( + (res.permissions || []).filter(p => p.isGranted).map(p => p.name || '') + ); + this.viewMode.set('edit'); + } + }); + } + + goToListMode() { + this.viewMode.set('list'); + this.selectedPermissions.set([]); + } + + // Provider selection + onProviderChange(providerName: string) { + this.selectedProviderName.set(providerName); + this.selectedProviderKey.set(''); + this.searchResults.set([]); + this.searchFilter.set(''); + } + + // Search + onSearchInput(filter: string) { + this.searchFilter.set(filter); + this.searchSubject.next(filter); + } + + private performSearch(filter: string) { + if (!filter || !this.selectedProviderName()) return; + + this.service.searchResourceProviderKey( + this.resourceName, + this.selectedProviderName(), + filter, + 1 + ).subscribe(res => { + this.searchResults.set(res.keys || []); + }); + } + + selectProviderKey(key: SearchProviderKeyInfo) { + this.selectedProviderKey.set(key.providerKey || ''); + this.searchFilter.set(key.providerDisplayName || key.providerKey || ''); + this.searchResults.set([]); + } + + // Permission toggle + togglePermission(permissionName: string) { + const current = this.selectedPermissions(); + if (current.includes(permissionName)) { + this.selectedPermissions.set(current.filter(p => p !== permissionName)); + } else { + this.selectedPermissions.set([...current, permissionName]); + } + } + + toggleAllPermissions(selectAll: boolean) { + if (this.viewMode() === 'add') { + this.selectedPermissions.set( + selectAll + ? this.permissionDefinitions().map(p => p.name || '') + : [] + ); + } else { + this.selectedPermissions.set( + selectAll + ? this.permissionsWithProvider().map(p => p.name || '') + : [] + ); + } + } + + isPermissionSelected(permissionName: string): boolean { + return this.selectedPermissions().includes(permissionName); + } + + allPermissionsSelected = computed(() => { + const definitions = this.viewMode() === 'add' + ? this.permissionDefinitions() + : this.permissionsWithProvider(); + return definitions.length > 0 && + definitions.every(p => this.selectedPermissions().includes(p.name || '')); + }); + + // Save operations + saveAddPermission() { + if (!this.selectedProviderKey() || this.selectedPermissions().length === 0) { + this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions'); + return; + } + + this.modalBusy.set(true); + this.service.updateResource( + this.resourceName, + this.resourceKey, + { + providerName: this.selectedProviderName(), + providerKey: this.selectedProviderKey(), + permissions: this.selectedPermissions() + } + ).pipe( + switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)), + finalize(() => this.modalBusy.set(false)) + ).subscribe({ + next: res => { + this.resourcePermissions.set(res.permissions || []); + this.toasterService.success('AbpUi::SavedSuccessfully'); + this.goToListMode(); + } + }); + } + + saveEditPermission() { + this.modalBusy.set(true); + this.service.updateResource( + this.resourceName, + this.resourceKey, + { + providerName: this.editProviderName(), + providerKey: this.editProviderKey(), + permissions: this.selectedPermissions() + } + ).pipe( + switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)), + finalize(() => this.modalBusy.set(false)) + ).subscribe({ + next: res => { + this.resourcePermissions.set(res.permissions || []); + this.toasterService.success('AbpUi::SavedSuccessfully'); + this.goToListMode(); + } + }); + } + + deletePermission(grant: ResourcePermissionGrantInfoDto) { + this.modalBusy.set(true); + this.service.deleteResource( + this.resourceName, + this.resourceKey, + grant.providerName || '', + grant.providerKey || '' + ).pipe( + switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)), + finalize(() => this.modalBusy.set(false)) + ).subscribe({ + next: res => { + this.resourcePermissions.set(res.permissions || []); + this.toasterService.success('AbpUi::SuccessfullyDeleted'); + } + }); + } +}