mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	disable document form components when no object permissions
This commit is contained in:
		| @@ -106,6 +106,7 @@ import localeTr from '@angular/common/locales/tr' | ||||
| import localeZh from '@angular/common/locales/zh' | ||||
| import { ShareUserComponent } from './components/common/input/share-user/share-user.component' | ||||
| import { IfOwnerDirective } from './directives/if-owner.directive' | ||||
| import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive' | ||||
|  | ||||
| registerLocaleData(localeBe) | ||||
| registerLocaleData(localeCs) | ||||
| @@ -199,6 +200,7 @@ function initializeApp(settings: SettingsService) { | ||||
|     MailRuleEditDialogComponent, | ||||
|     ShareUserComponent, | ||||
|     IfOwnerDirective, | ||||
|     IfObjectPermissionsDirective, | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|     <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> | ||||
|     <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check> | ||||
|  | ||||
|     <div *ifOwner="object.owner"> | ||||
|     <div *ifOwner="object?.owner"> | ||||
|       <h5 i18n>Permissions</h5> | ||||
|       <div formGroupName="set_permissions"> | ||||
|         <app-share-user type="view" formControlName="view"></app-share-user> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|       <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> | ||||
|       <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> | ||||
|  | ||||
|       <div *ifOwner="object.owner"> | ||||
|       <div *ifOwner="object?.owner"> | ||||
|         <h5 i18n>Permissions</h5> | ||||
|         <div formGroupName="set_permissions"> | ||||
|           <app-share-user type="view" formControlName="view"></app-share-user> | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|     <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> | ||||
|     <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> | ||||
|  | ||||
|     <div *ifOwner="object.owner"> | ||||
|     <div *ifOwner="object?.owner"> | ||||
|       <h5 i18n>Permissions</h5> | ||||
|       <div formGroupName="set_permissions"> | ||||
|         <app-share-user type="view" formControlName="view"></app-share-user> | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
|       <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text> | ||||
|       <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> | ||||
|  | ||||
|       <div *ifOwner="object.owner"> | ||||
|       <div *ifOwner="object?.owner"> | ||||
|         <h5 i18n>Permissions</h5> | ||||
|         <div formGroupName="set_permissions"> | ||||
|           <app-share-user type="view" formControlName="view"></app-share-user> | ||||
|   | ||||
| @@ -3,8 +3,8 @@ | ||||
|   <div class="input-group" [class.is-invalid]="error"> | ||||
|     <input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10" | ||||
|           (dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)" | ||||
|           name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel"> | ||||
|     <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button"> | ||||
|           name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled"> | ||||
|     <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled"> | ||||
|       <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16"> | ||||
|         <path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> | ||||
|       </svg> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <div class="mb-3"> | ||||
|   <label class="form-label" [for]="inputId">{{title}}</label> | ||||
|   <div class="input-group" [class.is-invalid]="error"> | ||||
|     <input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error"> | ||||
|     <button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button> | ||||
|     <input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled"> | ||||
|     <button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button> | ||||
|   </div> | ||||
|   <div class="invalid-feedback"> | ||||
|     {{error}} | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|         (clear)="clearLastSearchTerm()" | ||||
|         (blur)="onBlur()"> | ||||
|       </ng-select> | ||||
|       <button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()"> | ||||
|       <button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|         </svg> | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|  | ||||
|   <div class="input-group flex-nowrap"> | ||||
|     <ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value" | ||||
|       [disabled]="disabled" | ||||
|       [multiple]="true" | ||||
|       [closeOnSelect]="false" | ||||
|       [clearSearchOnAdd]="true" | ||||
| @@ -31,7 +32,7 @@ | ||||
|       </ng-template> | ||||
|     </ng-select> | ||||
|  | ||||
|     <button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()"> | ||||
|     <button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled"> | ||||
|       <svg class="buttonicon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|       </svg> | ||||
|   | ||||
| @@ -74,6 +74,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor { | ||||
|   } | ||||
|  | ||||
|   removeTag(id) { | ||||
|     if (this.disabled) return | ||||
|     let index = this.value.indexOf(id) | ||||
|     if (index > -1) { | ||||
|       let oldValue = this.value | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <div class="mb-3"> | ||||
|   <label class="form-label" [for]="inputId">{{title}}</label> | ||||
|   <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)"> | ||||
|   <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled"> | ||||
|   <small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> | ||||
|   <div class="invalid-feedback"> | ||||
|     {{error}} | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|       <div class="input-group-text" i18n>of {{previewNumPages}}</div> | ||||
|     </div> | ||||
|  | ||||
|     <button type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()" *ifPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }"> | ||||
|     <button *ifOwner="document?.owner" type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|         </svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span> | ||||
| @@ -28,7 +28,7 @@ | ||||
|  | ||||
|     </div> | ||||
|  | ||||
|     <button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()"> | ||||
|     <button *ifOwner="document?.owner" type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" /> | ||||
|         </svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span> | ||||
| @@ -191,9 +191,11 @@ | ||||
|  | ||||
|             <div [ngbNavOutlet]="nav" class="mt-2"></div> | ||||
|  | ||||
|             <button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || !(isDirty$ | async)">Discard</button>  | ||||
|             <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save & next</button>  | ||||
|             <button type="submit" class="btn btn-primary" *ifPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save</button>  | ||||
|             <ng-container action="PermissionAction.Change" *ifObjectPermissions="document"> | ||||
|                 <button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || !(isDirty$ | async)">Discard</button>  | ||||
|                 <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save & next</button>  | ||||
|                 <button type="submit" class="btn btn-primary" *ifPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save</button>  | ||||
|             </ng-container> | ||||
|         </form> | ||||
|     </div> | ||||
|  | ||||
|   | ||||
| @@ -306,6 +306,7 @@ export class DocumentDetailComponent | ||||
|         .map((p) => p[0]), | ||||
|     } | ||||
|     this.documentForm.patchValue(doc) | ||||
|     if (!this.userCanEdit) this.documentForm.disable() | ||||
|   } | ||||
|  | ||||
|   createDocumentType(newName: string) { | ||||
| @@ -591,4 +592,14 @@ export class DocumentDetailComponent | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   get userCanEdit(): boolean { | ||||
|     return ( | ||||
|       !this.document || | ||||
|       this.permissionsService.currentUserHasObjectPermissions( | ||||
|         PermissionAction.Change, | ||||
|         this.document | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										54
									
								
								src-ui/src/app/directives/if-object-permissions.directive.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src-ui/src/app/directives/if-object-permissions.directive.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| import { | ||||
|   Directive, | ||||
|   Input, | ||||
|   OnChanges, | ||||
|   OnInit, | ||||
|   TemplateRef, | ||||
|   ViewContainerRef, | ||||
| } from '@angular/core' | ||||
| import { ObjectWithPermissions } from '../data/object-with-permissions' | ||||
| import { | ||||
|   PermissionAction, | ||||
|   PermissionsService, | ||||
| } from '../services/permissions.service' | ||||
|  | ||||
| @Directive({ | ||||
|   selector: '[ifObjectPermissions]', | ||||
| }) | ||||
| export class IfObjectPermissionsDirective implements OnInit, OnChanges { | ||||
|   // The role the user must have | ||||
|   @Input() | ||||
|   ifObjectPermissions: ObjectWithPermissions | ||||
|  | ||||
|   @Input() | ||||
|   action: PermissionAction | ||||
|  | ||||
|   /** | ||||
|    * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef | ||||
|    * @param {TemplateRef<any>} templateRef -- The templateRef to be potentially rendered | ||||
|    * @param {PermissionsService} permissionsService -- Will give us access to the permissions a user has | ||||
|    */ | ||||
|   constructor( | ||||
|     private viewContainerRef: ViewContainerRef, | ||||
|     private templateRef: TemplateRef<any>, | ||||
|     private permissionsService: PermissionsService | ||||
|   ) {} | ||||
|  | ||||
|   public ngOnInit(): void { | ||||
|     if ( | ||||
|       !this.ifObjectPermissions || | ||||
|       this.permissionsService.currentUserHasObjectPermissions( | ||||
|         this.action, | ||||
|         this.ifObjectPermissions | ||||
|       ) | ||||
|     ) { | ||||
|       this.viewContainerRef.createEmbeddedView(this.templateRef) | ||||
|     } else { | ||||
|       this.viewContainerRef.clear() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public ngOnChanges(): void { | ||||
|     this.ngOnInit() | ||||
|   } | ||||
| } | ||||
| @@ -7,7 +7,7 @@ import { | ||||
|   ViewContainerRef, | ||||
| } from '@angular/core' | ||||
| import { PaperlessUser } from '../data/paperless-user' | ||||
| import { SettingsService } from '../services/settings.service' | ||||
| import { PermissionsService } from '../services/permissions.service' | ||||
|  | ||||
| @Directive({ | ||||
|   selector: '[ifOwner]', | ||||
| @@ -25,11 +25,14 @@ export class IfOwnerDirective implements OnInit, OnChanges { | ||||
|   constructor( | ||||
|     private viewContainerRef: ViewContainerRef, | ||||
|     private templateRef: TemplateRef<any>, | ||||
|     private settings: SettingsService | ||||
|     private permissionsService: PermissionsService | ||||
|   ) {} | ||||
|  | ||||
|   public ngOnInit(): void { | ||||
|     if (!this.ifOwner || this.ifOwner?.id === this.settings.currentUser.id) { | ||||
|     if ( | ||||
|       !this.ifOwner || | ||||
|       this.permissionsService.currentUserIsOwner(this.ifOwner) | ||||
|     ) { | ||||
|       this.viewContainerRef.createEmbeddedView(this.templateRef) | ||||
|     } else { | ||||
|       this.viewContainerRef.clear() | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import { Injectable } from '@angular/core' | ||||
| import { ObjectWithPermissions } from '../data/object-with-permissions' | ||||
| import { PaperlessUser } from '../data/paperless-user' | ||||
|  | ||||
| export enum PermissionAction { | ||||
|   Add = 'add', | ||||
| @@ -33,15 +35,30 @@ export interface PaperlessPermission { | ||||
| }) | ||||
| export class PermissionsService { | ||||
|   private permissions: string[] | ||||
|   private currentUser: PaperlessUser | ||||
|  | ||||
|   public initialize(permissions: string[]) { | ||||
|   public initialize(permissions: string[], currentUser: PaperlessUser) { | ||||
|     this.permissions = permissions | ||||
|     this.currentUser = currentUser | ||||
|   } | ||||
|  | ||||
|   public currentUserCan(permission: PaperlessPermission): boolean { | ||||
|     return this.permissions.includes(this.getPermissionCode(permission)) | ||||
|   } | ||||
|  | ||||
|   public currentUserIsOwner(owner: PaperlessUser): boolean { | ||||
|     return owner?.id === this.currentUser.id | ||||
|   } | ||||
|  | ||||
|   public currentUserHasObjectPermissions( | ||||
|     action: string, | ||||
|     object: ObjectWithPermissions | ||||
|   ): boolean { | ||||
|     return (object.permissions[action] as Array<number>)?.includes( | ||||
|       this.currentUser.id | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   public getPermissionCode(permission: PaperlessPermission): string { | ||||
|     return permission.type.replace('%s', permission.action) | ||||
|   } | ||||
|   | ||||
| @@ -79,7 +79,10 @@ export class SettingsService { | ||||
|           id: uisettings['user_id'], | ||||
|           username: uisettings['username'], | ||||
|         } | ||||
|         this.permissionsService.initialize(uisettings.permissions) | ||||
|         this.permissionsService.initialize( | ||||
|           uisettings.permissions, | ||||
|           this.currentUser | ||||
|         ) | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|   | ||||
| @@ -255,6 +255,7 @@ a, a:hover, | ||||
| .paperless-input-tags { | ||||
|   .ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value { | ||||
|     background-color: transparent; | ||||
|     border-color: transparent; | ||||
|   } | ||||
|  | ||||
|   .ng-select.ng-select-multiple .ng-select-container .ng-value-container { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon