diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 083976f28..01f721622 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -125,6 +125,9 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp PDF: { enabled: false, }, + STATS: { + enabled: false, + }, }, } as ILoggerConfig, }, diff --git a/apps/red-ui/src/app/guards/permissions-guard.ts b/apps/red-ui/src/app/guards/permissions-guard.ts new file mode 100644 index 000000000..6e43decd2 --- /dev/null +++ b/apps/red-ui/src/app/guards/permissions-guard.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate } from '@angular/router'; +import { firstValueFrom } from 'rxjs'; +import { EntityPermissionsService } from '../services/entity-permissions/entity-permissions.service'; + +@Injectable({ providedIn: 'root' }) +export class PermissionsGuard implements CanActivate { + constructor(private readonly _entityPermissionsService: EntityPermissionsService) {} + + async canActivate(route: ActivatedRouteSnapshot): Promise { + const targetObject = route.data.permissionsObject; + await firstValueFrom(this._entityPermissionsService.loadConfigFor(targetObject)); + await firstValueFrom(this._entityPermissionsService.loadFor(targetObject)); + return true; + } +} diff --git a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts index 0f1446a68..73b51330a 100644 --- a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts @@ -24,6 +24,7 @@ import { DossierStatesListingScreenComponent } from './screens/dossier-states-li import { DossiersGuard } from '@guards/dossiers.guard'; import { ACTIVE_DOSSIERS_SERVICE } from '../../tokens'; import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component'; +import { PermissionsGuard } from '../../guards/permissions-guard'; const routes: Routes = [ { path: '', redirectTo: 'dossier-templates', pathMatch: 'full' }, @@ -159,6 +160,16 @@ const routes: Routes = [ requiredRoles: ['RED_USER_ADMIN'], }, }, + { + path: 'dossier-permissions', + component: BaseAdminScreenComponent, + canActivate: [CompositeRouteGuard], + data: { + routeGuards: [AuthGuard, RedRoleGuard, PermissionsGuard], + permissionsObject: 'Dossier', + }, + loadChildren: () => import('./screens/permissions/permissions.module').then(m => m.PermissionsModule), + }, { path: 'license-info', component: LicenseInformationScreenComponent, diff --git a/apps/red-ui/src/app/modules/admin/admin-side-nav/admin-side-nav.component.ts b/apps/red-ui/src/app/modules/admin/admin-side-nav/admin-side-nav.component.ts index 94d8f5d60..b5a1672de 100644 --- a/apps/red-ui/src/app/modules/admin/admin-side-nav/admin-side-nav.component.ts +++ b/apps/red-ui/src/app/modules/admin/admin-side-nav/admin-side-nav.component.ts @@ -44,6 +44,7 @@ export class AdminSideNavComponent implements OnInit { }, { screen: 'audit', label: _('admin-side-nav.audit'), hideIf: !this.currentUser.isAdmin }, { screen: 'users', label: _('admin-side-nav.user-management'), hideIf: !this.currentUser.isUserAdmin }, + { screen: 'dossier-permissions', label: _('dossier-permissions'), hideIf: !this.currentUser.isAdmin }, { screen: 'general-config', label: _('admin-side-nav.configurations'), diff --git a/apps/red-ui/src/app/modules/admin/screens/permissions/config.service.ts b/apps/red-ui/src/app/modules/admin/screens/permissions/config.service.ts new file mode 100644 index 000000000..d4b22c715 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/permissions/config.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { TableColumnConfig } from '@iqser/common-ui'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { PermissionsMapping } from '@red/domain'; +import { PermissionsConfigurationMapService } from '@services/entity-permissions/permissions-configuration-map.service'; +import { permissionsTranslations } from '../../translations/permissions-translations'; + +@Injectable() +export class ConfigService { + constructor(private readonly _permissionsConfigurationMapService: PermissionsConfigurationMapService) {} + + tableConfig(targetObject: string): TableColumnConfig[] { + const columns = this._permissionsConfigurationMapService.getMappedPermissions(targetObject).map(p => ({ + label: permissionsTranslations.mapped[p], + class: 'flex-center', + })); + + return [{ label: _('permissions-screen.table-col-names.permission') }, ...columns]; + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.html new file mode 100644 index 000000000..abbb71cdf --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.html @@ -0,0 +1,20 @@ + + + + + +
+
{{ translations[targetObject][config.searchKey] | translate }}
+
+ +
+
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.scss new file mode 100644 index 000000000..fdab5672e --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.scss @@ -0,0 +1,4 @@ +:host { + flex-grow: 1; + overflow: hidden; +} diff --git a/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.ts new file mode 100644 index 000000000..d39ec4380 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions-screen/permissions-screen.component.ts @@ -0,0 +1,64 @@ +import { ChangeDetectionStrategy, Component, forwardRef, Injector } from '@angular/core'; +import { DefaultListingServices, ListingComponent, LoadingService, SortingOrders, TableColumnConfig } from '@iqser/common-ui'; +import { PermissionsMapping } from '@red/domain'; +import { ConfigService } from '../config.service'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { EntityPermissionsService } from '@services/entity-permissions/entity-permissions.service'; +import { ActivatedRoute } from '@angular/router'; +import { PermissionsMapService } from '@services/entity-permissions/permissions-map.service'; +import { PermissionsConfigurationMapService } from '@services/entity-permissions/permissions-configuration-map.service'; +import { firstValueFrom } from 'rxjs'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { tap } from 'rxjs/operators'; +import { permissionsTranslations } from '../../../translations/permissions-translations'; +import { UserService } from '@services/user.service'; +import { RouterHistoryService } from '@services/router-history.service'; + +@UntilDestroy() +@Component({ + templateUrl: './permissions-screen.component.html', + styleUrls: ['./permissions-screen.component.scss'], + providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => PermissionsScreenComponent) }], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PermissionsScreenComponent extends ListingComponent { + readonly currentUser = this._userService.currentUser; + readonly translations = permissionsTranslations; + readonly tableColumnConfigs: TableColumnConfig[]; + readonly tableHeaderLabel = _('permissions-screen.table-header.title'); + readonly targetObject: string; + readonly mappedPermissions: string[]; + + constructor( + protected readonly _injector: Injector, + private readonly _configService: ConfigService, + private readonly _userService: UserService, + private readonly _loadingService: LoadingService, + private readonly _entityPermissionsService: EntityPermissionsService, + private readonly _permissionsMapService: PermissionsMapService, + private readonly _permissionsConfigurationMapService: PermissionsConfigurationMapService, + private readonly _route: ActivatedRoute, + readonly routerHistoryService: RouterHistoryService, + ) { + super(_injector); + this.targetObject = _route.snapshot.data.permissionsObject; + this.tableColumnConfigs = this._configService.tableConfig(this.targetObject); + this.mappedPermissions = this._permissionsConfigurationMapService.getMappedPermissions(this.targetObject); + this.entitiesService.setEntities(this._permissionsMapService.get(this.targetObject)); + this.sortingService.setSortingOption({ + column: 'sort', + order: SortingOrders.asc, + }); + this._permissionsMapService + .get$(this.targetObject) + .pipe(tap(permissions => this.entitiesService.setEntities(permissions))) + .pipe(untilDestroyed(this)) + .subscribe(); + } + + async togglePermission(targetPermission: string, changedPermission: string): Promise { + this._loadingService.start(); + await firstValueFrom(this._entityPermissionsService.togglePermission(this.targetObject, targetPermission, changedPermission)); + this._loadingService.stop(); + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/permissions/permissions.module.ts b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions.module.ts new file mode 100644 index 000000000..f96d895c5 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/permissions/permissions.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { SharedModule } from '@shared/shared.module'; +import { PermissionsScreenComponent } from './permissions-screen/permissions-screen.component'; +import { ConfigService } from './config.service'; +import { CommonUiModule } from '@iqser/common-ui'; + +const routes = [{ path: '', component: PermissionsScreenComponent }]; + +@NgModule({ + declarations: [PermissionsScreenComponent], + imports: [RouterModule.forChild(routes), CommonModule, SharedModule, CommonUiModule], + providers: [ConfigService], +}) +export class PermissionsModule {} diff --git a/apps/red-ui/src/app/modules/admin/translations/permissions-translations.ts b/apps/red-ui/src/app/modules/admin/translations/permissions-translations.ts new file mode 100644 index 000000000..73f904abb --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/translations/permissions-translations.ts @@ -0,0 +1,14 @@ +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; + +export const permissionsTranslations: Record> = { + mapped: { + OWNER: _('permissions-screen.mapped.owner'), + APPROVE: _('permissions-screen.mapped.approve'), + REVIEW: _('permissions-screen.mapped.review'), + EVERYONE_ELSE: _('permissions-screen.mapped.everyone-else'), + }, + Dossier: { + VIEW_OBJECT: _('permissions-screen.dossier.view'), + ACCESS_OBJECT: _('permissions-screen.dossier.access'), + }, +} as const; diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts index c31751bdd..98c6d1940 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-details/annotation-details.component.ts @@ -57,18 +57,18 @@ export class AnnotationDetailsComponent implements OnChanges { [ { icon: 'red:dictionary', - description: 'annotation-engines.dictionary', + description: _('annotation-engines.dictionary'), show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.DICTIONARY), translateParams: { isHint: this.annotation.hint }, }, { icon: 'red:ai', - description: 'annotation-engines.ner', + description: _('annotation-engines.ner'), show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.NER), }, { icon: 'red:rule', - description: 'annotation-engines.rule', + description: _('annotation-engines.rule'), show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.RULE), translateParams: { rule: this.annotation.legalBasisValue || '' }, }, diff --git a/apps/red-ui/src/app/services/entity-permissions/entity-permissions.service.ts b/apps/red-ui/src/app/services/entity-permissions/entity-permissions.service.ts new file mode 100644 index 000000000..1881672f6 --- /dev/null +++ b/apps/red-ui/src/app/services/entity-permissions/entity-permissions.service.ts @@ -0,0 +1,49 @@ +import { Injectable, Injector } from '@angular/core'; +import { GenericService, mapEach } from '@iqser/common-ui'; +import { IPermissionsMapping, PermissionsMapping } from '@red/domain'; +import { Observable } from 'rxjs'; +import { switchMap, tap } from 'rxjs/operators'; +import { PermissionsConfigurationMapService } from './permissions-configuration-map.service'; +import { PermissionsMapService } from './permissions-map.service'; + +@Injectable({ + providedIn: 'root', +}) +export class EntityPermissionsService extends GenericService { + constructor( + protected readonly _injector: Injector, + private readonly _permissionsConfigurationMapService: PermissionsConfigurationMapService, + private readonly _permissionsMapService: PermissionsMapService, + ) { + super(_injector, 'permissions'); + } + + loadConfigFor(targetObject: string): Observable { + return this._http.get(`/${this._defaultModelPath}/${targetObject}/mapping`).pipe( + mapEach(mapping => new PermissionsMapping(mapping, targetObject)), + tap(mappings => this._permissionsConfigurationMapService.set(targetObject, mappings)), + ); + } + + loadFor(targetObject: string): Observable { + return this._http.get(`/${this._defaultModelPath}/${targetObject}`).pipe( + mapEach(mapping => new PermissionsMapping(mapping, targetObject)), + tap(mappings => this._permissionsMapService.set(targetObject, mappings)), + ); + } + + togglePermission(targetObject: string, targetPermission: string, changedPermission: string): Observable { + const config = this._permissionsConfigurationMapService.get(targetObject); + const targetPermissionConfig = config.find(p => p.searchKey === targetPermission); + const permissions = this._permissionsMapService.get(targetObject); + const currentTargetPermissionValues = this._permissionsMapService.get(targetObject, targetPermission); + const index = currentTargetPermissionValues.mappedPermissions.findIndex(p => p.name === changedPermission); + if (index !== -1) { + currentTargetPermissionValues.mappedPermissions.splice(index, 1); + } else { + const permission = targetPermissionConfig.mappedPermissions.find(p => p.name === changedPermission); + currentTargetPermissionValues.mappedPermissions.push(permission); + } + return this._post(permissions, `${this._defaultModelPath}/${targetObject}`).pipe(switchMap(() => this.loadFor(targetObject))); + } +} diff --git a/apps/red-ui/src/app/services/entity-permissions/permissions-configuration-map.service.ts b/apps/red-ui/src/app/services/entity-permissions/permissions-configuration-map.service.ts new file mode 100644 index 000000000..7000d1056 --- /dev/null +++ b/apps/red-ui/src/app/services/entity-permissions/permissions-configuration-map.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { IPermissionsMapping, PermissionsMapping } from '@red/domain'; +import { EntitiesMapService } from '@iqser/common-ui'; + +@Injectable({ providedIn: 'root' }) +export class PermissionsConfigurationMapService extends EntitiesMapService { + constructor() { + super('name'); + } + + getMappedPermissions(targetObject: string): string[] { + return Array.from( + new Set( + this.get(targetObject) + .flatMap(p => p.mappedPermissions) + .map(p => p.name), + ), + ); + } +} diff --git a/apps/red-ui/src/app/services/entity-permissions/permissions-map.service.ts b/apps/red-ui/src/app/services/entity-permissions/permissions-map.service.ts new file mode 100644 index 000000000..b2f63f9ed --- /dev/null +++ b/apps/red-ui/src/app/services/entity-permissions/permissions-map.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; +import { IPermissionsMapping, PermissionsMapping } from '@red/domain'; +import { EntitiesMapService } from '@iqser/common-ui'; + +@Injectable({ providedIn: 'root' }) +export class PermissionsMapService extends EntitiesMapService { + constructor() { + super('name'); + } +} diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index bc57ba1c8..20b238732 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -864,6 +864,7 @@ "under-review": "In Review", "upload-files": "Sie können Dateien überall per Drag and Drop platzieren..." }, + "dossier-permissions": "", "dossier-states-listing": { "action": { "delete": "", @@ -1626,6 +1627,25 @@ }, "toggle-tooltips": "{active, select, true{Disable} false{Enable} other{}} Kurzinfos für Anmerkungen" }, + "permissions-screen": { + "dossier": { + "access": "", + "view": "" + }, + "label": "", + "mapped": { + "approve": "", + "everyone-else": "", + "owner": "", + "review": "" + }, + "table-col-names": { + "permission": "" + }, + "table-header": { + "title": "" + } + }, "processing-status": { "ocr": "", "pending": "", diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index f2cf3b269..78453e31e 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -864,6 +864,7 @@ "under-review": "Under Review", "upload-files": "Drag & drop files anywhere..." }, + "dossier-permissions": "Dossier Permissions", "dossier-states-listing": { "action": { "delete": "Delete State", @@ -1626,6 +1627,25 @@ }, "toggle-tooltips": "{active, select, true{Disable} false{Enable} other{}} annotation tooltips" }, + "permissions-screen": { + "dossier": { + "access": "Access Dossier", + "view": "View Dossier" + }, + "label": "{targetObject, select, Dossier{Dossier} other{}} Permissions", + "mapped": { + "approve": "Approvers", + "everyone-else": "Everyone else", + "owner": "Owner", + "review": "Reviewers" + }, + "table-col-names": { + "permission": "Permission" + }, + "table-header": { + "title": "{length} {length, plural, one{Permission} other{Permissions}}" + } + }, "processing-status": { "ocr": "OCR", "pending": "Pending", diff --git a/libs/red-domain/src/index.ts b/libs/red-domain/src/index.ts index 99077ba8e..bd2f2a0df 100644 --- a/libs/red-domain/src/index.ts +++ b/libs/red-domain/src/index.ts @@ -23,3 +23,4 @@ export * from './lib/dossier-stats'; export * from './lib/dossier-state'; export * from './lib/trash'; export * from './lib/text-highlight'; +export * from './lib/permissions'; diff --git a/libs/red-domain/src/lib/permissions/index.ts b/libs/red-domain/src/lib/permissions/index.ts new file mode 100644 index 000000000..bb52be96a --- /dev/null +++ b/libs/red-domain/src/lib/permissions/index.ts @@ -0,0 +1,3 @@ +export * from './permissions-mapping'; +export * from './permissions'; +export * from './permissions-mapping.model'; diff --git a/libs/red-domain/src/lib/permissions/permissions-mapping.model.ts b/libs/red-domain/src/lib/permissions/permissions-mapping.model.ts new file mode 100644 index 000000000..d06839806 --- /dev/null +++ b/libs/red-domain/src/lib/permissions/permissions-mapping.model.ts @@ -0,0 +1,32 @@ +import { Entity } from '@iqser/common-ui'; +import { IPermissionsMapping } from './permissions-mapping'; +import { IPermission } from './permissions'; + +export class PermissionsMapping extends Entity implements IPermissionsMapping { + readonly routerLink = undefined; + readonly mappedPermissions: IPermission[]; + readonly targetPermission: IPermission; + readonly sort: number; + + readonly #currentValuesMap = new Map(); + + constructor(permissionsMapping: IPermissionsMapping, readonly targetObject: string) { + super(permissionsMapping); + this.mappedPermissions = permissionsMapping.mappedPermissions; + this.targetPermission = permissionsMapping.targetPermission; + this.sort = this.targetPermission.sort; + this.mappedPermissions.forEach(permission => this.#currentValuesMap.set(permission.name, true)); + } + + get id(): string { + return this.targetPermission.name; + } + + get searchKey(): string { + return this.targetPermission.name; + } + + getValue(permissionName: string): boolean { + return this.#currentValuesMap.has(permissionName); + } +} diff --git a/libs/red-domain/src/lib/permissions/permissions-mapping.ts b/libs/red-domain/src/lib/permissions/permissions-mapping.ts new file mode 100644 index 000000000..1baf6a87a --- /dev/null +++ b/libs/red-domain/src/lib/permissions/permissions-mapping.ts @@ -0,0 +1,6 @@ +import { IPermission } from './permissions'; + +export interface IPermissionsMapping { + mappedPermissions: IPermission[]; + targetPermission: IPermission; +} diff --git a/libs/red-domain/src/lib/permissions/permissions.ts b/libs/red-domain/src/lib/permissions/permissions.ts new file mode 100644 index 000000000..8efa79e79 --- /dev/null +++ b/libs/red-domain/src/lib/permissions/permissions.ts @@ -0,0 +1,5 @@ +export interface IPermission { + mask: number; + name: string; + sort: number; +}