Skip to content

Commit cab33b9

Browse files
committed
feat: Implement audit logging for POD view and download actions
1 parent ffcef3e commit cab33b9

3 files changed

Lines changed: 92 additions & 2 deletions

File tree

ShopInventory.Web/Components/Pages/ProofOfDelivery.razor

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,10 +1949,21 @@ filterUploadedFromLocation.Trim();
19491949
{
19501950
var url = $"/download/pod/{pod.InvoiceDocEntry}/{pod.Id}";
19511951
await JS.InvokeVoidAsync("downloadAuthenticatedFile", url, pod.FileName);
1952+
await LogPodAuditAsync(
1953+
AuditActions.DownloadPod,
1954+
pod,
1955+
true,
1956+
$"Downloaded POD '{pod.FileName}' for {GetPodInvoiceLabel(pod)}.");
19521957
}
19531958
catch (Exception ex)
19541959
{
19551960
Logger.LogError(ex, "Error downloading POD {PodId}", pod.Id);
1961+
await LogPodAuditAsync(
1962+
AuditActions.DownloadPod,
1963+
pod,
1964+
false,
1965+
$"Failed to download POD '{pod.FileName}' for {GetPodInvoiceLabel(pod)}.",
1966+
ex.Message);
19561967
}
19571968
}
19581969

@@ -1976,10 +1987,22 @@ filterUploadedFromLocation.Trim();
19761987
viewerDataUri = await JS.InvokeAsync<string>(
19771988
"createAuthenticatedObjectUrl",
19781989
$"/download/pod/{pod.InvoiceDocEntry}/{pod.Id}");
1990+
1991+
await LogPodAuditAsync(
1992+
AuditActions.ViewPod,
1993+
pod,
1994+
true,
1995+
$"Viewed POD '{pod.FileName}' for {GetPodInvoiceLabel(pod)}.");
19791996
}
19801997
catch (Exception ex)
19811998
{
19821999
Logger.LogError(ex, "Error viewing POD {PodId}", pod.Id);
2000+
await LogPodAuditAsync(
2001+
AuditActions.ViewPod,
2002+
pod,
2003+
false,
2004+
$"Failed to view POD '{pod.FileName}' for {GetPodInvoiceLabel(pod)}.",
2005+
ex.Message);
19832006
showViewer = false;
19842007
}
19852008
finally
@@ -2005,6 +2028,36 @@ filterUploadedFromLocation.Trim();
20052028
}
20062029
}
20072030

2031+
private async Task LogPodAuditAsync(
2032+
string action,
2033+
PodAttachmentItemDto pod,
2034+
bool isSuccess,
2035+
string details,
2036+
string? errorMessage = null)
2037+
{
2038+
try
2039+
{
2040+
await AuditService.LogAsync(
2041+
action,
2042+
"POD",
2043+
pod.Id.ToString(CultureInfo.InvariantCulture),
2044+
details,
2045+
isSuccess,
2046+
errorMessage);
2047+
}
2048+
catch (Exception ex)
2049+
{
2050+
Logger.LogWarning(ex, "Failed to write POD audit log for action {Action} and attachment {PodId}", action, pod.Id);
2051+
}
2052+
}
2053+
2054+
private static string GetPodInvoiceLabel(PodAttachmentItemDto pod)
2055+
{
2056+
return pod.InvoiceDocNum > 0
2057+
? $"invoice #{pod.InvoiceDocNum}"
2058+
: $"invoice doc entry {pod.InvoiceDocEntry}";
2059+
}
2060+
20082061
private void ConfirmDeletePod(PodAttachmentItemDto pod)
20092062
{
20102063
if (!canDeletePods) return;

ShopInventory.Web/Data/AuditLog.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ public static class AuditActions
169169
// POD actions
170170
public const string UploadPod = "UploadPod";
171171
public const string BulkUploadPod = "BulkUploadPod";
172+
public const string ViewPod = "ViewPod";
173+
public const string DownloadPod = "DownloadPod";
172174

173175
// Customer actions
174176
public const string ViewCustomers = "ViewCustomers";

ShopInventory/Features/Documents/DocumentAttachmentAccessService.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.EntityFrameworkCore;
44
using ShopInventory.Common.Crates;
55
using ShopInventory.Common.Errors;
6+
using ShopInventory.Common.Pods;
67
using ShopInventory.Data;
78
using ShopInventory.Models;
89
using ShopInventory.Models.Entities;
@@ -189,15 +190,49 @@ private async Task<ErrorOr<bool>> EnsureInvoiceAccessAsync(
189190
}
190191

191192
var scopedDocEntries = await documentService.GetScopedPodInvoiceDocEntriesAsync([entityId], assignedSection, cancellationToken);
192-
if (!scopedDocEntries.Contains(entityId))
193+
if (scopedDocEntries.Contains(entityId))
193194
{
194-
return Errors.Document.AccessDenied("This invoice is outside your assigned POD section.");
195+
return true;
196+
}
197+
198+
if (!isWriteOperation && await MatchesUploaderAssignedSectionAsync(uploadedByUserId, assignedSection, cancellationToken))
199+
{
200+
return true;
195201
}
202+
203+
return Errors.Document.AccessDenied("This invoice is outside your assigned POD section.");
196204
}
197205

198206
return true;
199207
}
200208

209+
private async Task<bool> MatchesUploaderAssignedSectionAsync(
210+
Guid? uploadedByUserId,
211+
string assignedSection,
212+
CancellationToken cancellationToken)
213+
{
214+
if (!uploadedByUserId.HasValue || string.IsNullOrWhiteSpace(assignedSection))
215+
{
216+
return false;
217+
}
218+
219+
var uploaderAssignedSection = await context.Users
220+
.AsNoTracking()
221+
.Where(user => user.Id == uploadedByUserId.Value)
222+
.Select(user => user.AssignedSection)
223+
.FirstOrDefaultAsync(cancellationToken);
224+
225+
if (string.IsNullOrWhiteSpace(uploaderAssignedSection))
226+
{
227+
return false;
228+
}
229+
230+
return string.Equals(
231+
PodLocationScope.CanonicalizeSection(uploaderAssignedSection),
232+
PodLocationScope.CanonicalizeSection(assignedSection),
233+
StringComparison.OrdinalIgnoreCase);
234+
}
235+
201236
private async Task<ErrorOr<bool>> EnsureExternalPurchaseOrderAccessAsync(
202237
string role,
203238
bool isWriteOperation,

0 commit comments

Comments
 (0)