diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index f990122dd..b3f8a0055 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -119,6 +119,7 @@ import { NgxFilesizeModule } from 'ngx-filesize' import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' +import { AuditLogComponent } from './components/audit-log/audit-log.component' import { airplane, archive, @@ -472,6 +473,7 @@ function initializeApp(settings: SettingsService) { RotateConfirmDialogComponent, MergeConfirmDialogComponent, SplitConfirmDialogComponent, + AuditLogComponent, ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/audit-log/audit-log.component.html b/src-ui/src/app/components/audit-log/audit-log.component.html new file mode 100644 index 000000000..d52787f36 --- /dev/null +++ b/src-ui/src/app/components/audit-log/audit-log.component.html @@ -0,0 +1,50 @@ + diff --git a/src-ui/src/app/components/audit-log/audit-log.component.scss b/src-ui/src/app/components/audit-log/audit-log.component.scss new file mode 100644 index 000000000..31a7d08d6 --- /dev/null +++ b/src-ui/src/app/components/audit-log/audit-log.component.scss @@ -0,0 +1,3 @@ +table { + transition: all 0.3s ease; +} diff --git a/src-ui/src/app/components/audit-log/audit-log.component.spec.ts b/src-ui/src/app/components/audit-log/audit-log.component.spec.ts new file mode 100644 index 000000000..8ae9d5bba --- /dev/null +++ b/src-ui/src/app/components/audit-log/audit-log.component.spec.ts @@ -0,0 +1,77 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { AuditLogComponent } from './audit-log.component' +import { DocumentService } from 'src/app/services/rest/document.service' +import { of } from 'rxjs' +import { AuditLogAction } from 'src/app/data/auditlog-entry' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' +import { DatePipe } from '@angular/common' +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' + +describe('AuditLogComponent', () => { + let component: AuditLogComponent + let fixture: ComponentFixture + let documentService: DocumentService + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AuditLogComponent, CustomDatePipe], + providers: [DatePipe], + imports: [ + HttpClientTestingModule, + NgbCollapseModule, + NgxBootstrapIconsModule.pick(allIcons), + ], + }).compileComponents() + + fixture = TestBed.createComponent(AuditLogComponent) + documentService = TestBed.inject(DocumentService) + component = fixture.componentInstance + }) + + it('should get audit log entries on init', () => { + const getAuditLogSpy = jest.spyOn(documentService, 'getAuditLog') + getAuditLogSpy.mockReturnValue( + of([ + { + id: 1, + actor: { + id: 1, + username: 'user1', + }, + action: AuditLogAction.Create, + timestamp: '2021-01-01T00:00:00Z', + remote_addr: '1.2.3.4', + changes: { + title: ['old title', 'new title'], + }, + }, + ]) + ) + component.documentId = 1 + fixture.detectChanges() + expect(getAuditLogSpy).toHaveBeenCalledWith(1) + }) + + it('should toggle entry', () => { + const entry = { + id: 1, + actor: { + id: 1, + username: 'user1', + }, + action: AuditLogAction.Create, + timestamp: '2021-01-01T00:00:00Z', + remote_addr: '1.2.3.4', + changes: { + title: ['old title', 'new title'], + }, + } + component.toggleEntry(entry) + expect(component.openEntries.has(1)).toBe(true) + component.toggleEntry(entry) + expect(component.openEntries.has(1)).toBe(false) + }) +}) diff --git a/src-ui/src/app/components/audit-log/audit-log.component.ts b/src-ui/src/app/components/audit-log/audit-log.component.ts new file mode 100644 index 000000000..21e264ea8 --- /dev/null +++ b/src-ui/src/app/components/audit-log/audit-log.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, OnInit } from '@angular/core' +import { AuditLogEntry } from 'src/app/data/auditlog-entry' +import { DocumentService } from 'src/app/services/rest/document.service' + +@Component({ + selector: 'pngx-audit-log', + templateUrl: './audit-log.component.html', + styleUrl: './audit-log.component.scss', +}) +export class AuditLogComponent implements OnInit { + _documentId: number + @Input() + set documentId(id: number) { + this._documentId = id + this.ngOnInit() + } + + public loading: boolean = true + public entries: AuditLogEntry[] = [] + public openEntries: Set = new Set() + + constructor(private documentService: DocumentService) {} + + ngOnInit(): void { + if (this._documentId) { + this.loading = true + this.documentService + .getAuditLog(this._documentId) + .subscribe((auditLogEntries) => { + this.entries = auditLogEntries + this.loading = false + }) + } + } + + toggleEntry(entry: AuditLogEntry) { + if (this.openEntries.has(entry.id)) { + this.openEntries.delete(entry.id) + } else { + this.openEntries.add(entry.id) + } + } +} diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 14b235fb7..b9796380a 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -285,6 +285,15 @@ } +
  • + History + +
    + +
    +
    +
  • + @if (showPermissions) {
  • Permissions diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index d8f63faf2..f1aa3fc7a 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -77,6 +77,7 @@ enum DocumentDetailNavIDs { Preview = 4, Notes = 5, Permissions = 6, + History = 7, } enum ContentRenderType { diff --git a/src-ui/src/app/data/auditlog-entry.ts b/src-ui/src/app/data/auditlog-entry.ts new file mode 100644 index 000000000..5181b9fd1 --- /dev/null +++ b/src-ui/src/app/data/auditlog-entry.ts @@ -0,0 +1,16 @@ +import { User } from './user' + +export enum AuditLogAction { + Create = 'create', + Update = 'update', + Delete = 'delete', +} + +export interface AuditLogEntry { + id: number + timestamp: string + action: AuditLogAction + changes: any + remote_addr: string + actor?: User +} diff --git a/src-ui/src/app/services/permissions.service.ts b/src-ui/src/app/services/permissions.service.ts index 0648f461f..56ed6fad8 100644 --- a/src-ui/src/app/services/permissions.service.ts +++ b/src-ui/src/app/services/permissions.service.ts @@ -19,6 +19,7 @@ export enum PermissionType { PaperlessTask = '%s_paperlesstask', AppConfig = '%s_applicationconfiguration', UISettings = '%s_uisettings', + AuditLogEntry = '%s_logentry', Note = '%s_note', MailAccount = '%s_mailaccount', MailRule = '%s_mailrule', diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts index 1f3ccc0af..c7c07d307 100644 --- a/src-ui/src/app/services/rest/document.service.spec.ts +++ b/src-ui/src/app/services/rest/document.service.spec.ts @@ -266,6 +266,13 @@ describe(`DocumentService`, () => { ) expect(req.request.body.remove_inbox_tags).toEqual(true) }) + + it('should call appropriate api endpoint for getting audit log', () => { + subscription = service.getAuditLog(documents[0].id).subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/audit/` + ) + }) }) afterEach(() => { diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 5c0f0a1dc..0233a81f4 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -20,6 +20,7 @@ import { } from '../permissions.service' import { SettingsService } from '../settings.service' import { SETTINGS, SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { AuditLogEntry } from 'src/app/data/auditlog-entry' export const DOCUMENT_SORT_FIELDS = [ { field: 'archive_serial_number', name: $localize`ASN` }, @@ -222,6 +223,10 @@ export class DocumentService extends AbstractPaperlessService { ) } + getAuditLog(id: number): Observable { + return this.http.get(this.getResourceUrl(id, 'audit')) + } + bulkDownload( ids: number[], content = 'both',