mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 13:03:12 +00:00
fix(work-orders): scope legacy list/detail GETs by account [SH-221]
Close the remaining SH-221 read gap so Getworkorders, filtered lists, and GetWorkorderById enforce the same server-derived account boundary as board/media. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bca2e0d50d
commit
62a4828e2f
6 changed files with 217 additions and 38 deletions
|
|
@ -193,10 +193,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
var accountId = _accountResolver.ResolveAccountFilter(User);
|
||||
var result = await _workOrderService.GetFilteredWorkOrdersAsync(
|
||||
assignto, location, priority, status, duedate, search, sort, sortby, page, pageSize);
|
||||
assignto, location, priority, status, duedate, search, sort, sortby, page, pageSize, accountId);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return StatusCode(500, "Internal Server Error");
|
||||
|
|
@ -207,10 +212,18 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpGet("GetWorkorderById")]
|
||||
public async Task<IActionResult> GetWorkorderById(int id)
|
||||
{
|
||||
var detail = await _workOrderService.GetWorkOrderDetailAsync(id);
|
||||
if (detail == null)
|
||||
return BadRequest(new Response { Status = "Error", Message = "ID not found!" });
|
||||
return Ok(detail);
|
||||
try
|
||||
{
|
||||
var accountId = _accountResolver.ResolveAccountFilter(User);
|
||||
var detail = await _workOrderService.GetWorkOrderDetailAsync(id, accountId);
|
||||
if (detail == null)
|
||||
return BadRequest(new Response { Status = "Error", Message = "ID not found!" });
|
||||
return Ok(detail);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
|
@ -229,10 +242,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
var accountId = _accountResolver.ResolveAccountFilter(User);
|
||||
var result = await _workOrderService.GetFilteredWorkOrders2Async(
|
||||
assignto, location, priority, status, duedate, search, sort, sortby, page, pageSize);
|
||||
assignto, location, priority, status, duedate, search, sort, sortby, page, pageSize, accountId);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
var totalCount = await _workOrderService.GetTotalWorkOrderCountAsync();
|
||||
|
|
@ -352,16 +370,32 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[Route("GetworkordersDD")]
|
||||
public async Task<IActionResult> GetworkordersDD()
|
||||
{
|
||||
var data = await _workOrderService.GetWorkordersDDAsync();
|
||||
return Ok(data);
|
||||
try
|
||||
{
|
||||
var accountId = _accountResolver.ResolveAccountFilter(User);
|
||||
var data = await _workOrderService.GetWorkordersDDAsync(accountId);
|
||||
return Ok(data);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Getworkorders")]
|
||||
public async Task<IActionResult> Getworkorders()
|
||||
{
|
||||
var data = await _workOrderService.GetWorkordersAsync();
|
||||
return Ok(data);
|
||||
try
|
||||
{
|
||||
var accountId = _accountResolver.ResolveAccountFilter(User);
|
||||
var data = await _workOrderService.GetWorkordersAsync(accountId);
|
||||
return Ok(data);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -373,10 +373,15 @@ namespace SeaHaven.DataServices.Implementation
|
|||
return await _context.workOrders.CountAsync(w => w.LocationId == locationId);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync()
|
||||
public async Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync(int? accountId = null)
|
||||
{
|
||||
return await _context.workOrders
|
||||
.Where(w => w.istemplate != true)
|
||||
IQueryable<WorkOrder> query = _context.workOrders
|
||||
.Where(w => w.istemplate != true);
|
||||
|
||||
if (accountId.HasValue)
|
||||
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
||||
|
||||
return await query
|
||||
.Include(w => w.Locations)
|
||||
.Include(w => w.AssignToUser)
|
||||
.AsSplitQuery()
|
||||
|
|
@ -408,11 +413,16 @@ namespace SeaHaven.DataServices.Implementation
|
|||
return ordered;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id)
|
||||
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id, int? accountId = null)
|
||||
{
|
||||
return await _context.workOrders
|
||||
var query = _context.workOrders
|
||||
.AsNoTracking()
|
||||
.Where(w => w.Id == id)
|
||||
.Where(w => w.Id == id);
|
||||
|
||||
if (accountId.HasValue)
|
||||
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
||||
|
||||
return await query
|
||||
.Select(s => new WorkOrderDetailReadModel
|
||||
{
|
||||
Id = s.Id,
|
||||
|
|
@ -509,10 +519,15 @@ namespace SeaHaven.DataServices.Implementation
|
|||
}).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync()
|
||||
public async Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync(int? accountId = null)
|
||||
{
|
||||
return await _context.workOrders
|
||||
.Where(w => w.istemplate != true)
|
||||
IQueryable<WorkOrder> query = _context.workOrders
|
||||
.Where(w => w.istemplate != true);
|
||||
|
||||
if (accountId.HasValue)
|
||||
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
||||
|
||||
return await query
|
||||
.Include(w => w.Locations)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<int> CountByStatusAsync(string status);
|
||||
Task<int> CountByPriorityAsync(string priority);
|
||||
Task<int> CountByLocationAsync(int locationId);
|
||||
Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync();
|
||||
Task<IEnumerable<WorkOrder>> GetAllWithDetailsAsync(int? accountId = null);
|
||||
|
||||
// Batched lookup: server-filters the distinct requested ids, excludes templates,
|
||||
// eagerly loads the location detail, deduplicates, and returns results in input order.
|
||||
|
|
@ -80,7 +80,7 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
CancellationToken cancellationToken);
|
||||
|
||||
// Complex read models
|
||||
Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id);
|
||||
Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync();
|
||||
Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id, int? accountId = null);
|
||||
Task<List<WorkOrder>> GetNonTemplateWorkOrdersWithLocationsAsync(int? accountId = null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,17 +138,17 @@ namespace SeaHaven.Services.Implementation
|
|||
page, pageSize, search, status, assignee, locationId, sortBy, sortDir, accountId);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id)
|
||||
public async Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id, int? accountId = null)
|
||||
{
|
||||
return await _workOrderDataService.GetWorkOrderDetailAsync(id);
|
||||
return await _workOrderDataService.GetWorkOrderDetailAsync(id, accountId);
|
||||
}
|
||||
|
||||
public async Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrdersAsync(
|
||||
string[] assignto, int[]? location, string[]? priority,
|
||||
string[]? status, string[]? duedate, string search,
|
||||
string sort, string sortby, int page, int pageSize)
|
||||
string sort, string sortby, int page, int pageSize, int? accountId = null)
|
||||
{
|
||||
var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync();
|
||||
var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync(accountId);
|
||||
|
||||
var data = allWorkOrders.AsEnumerable();
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ namespace SeaHaven.Services.Implementation
|
|||
public async Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrders2Async(
|
||||
string[] assignto, int[]? location, string[]? priority,
|
||||
string[]? status, string[]? duedate, string search,
|
||||
string sort, string sortby, int page, int pageSize)
|
||||
string sort, string sortby, int page, int pageSize, int? accountId = null)
|
||||
{
|
||||
location ??= Array.Empty<int>();
|
||||
priority ??= Array.Empty<string>();
|
||||
|
|
@ -238,7 +238,7 @@ namespace SeaHaven.Services.Implementation
|
|||
duedate ??= Array.Empty<string>();
|
||||
search ??= string.Empty;
|
||||
|
||||
var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync();
|
||||
var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync(accountId);
|
||||
var totalCount = allWorkOrders.Count;
|
||||
|
||||
var currentDate = DateTime.UtcNow.Date;
|
||||
|
|
@ -505,15 +505,15 @@ namespace SeaHaven.Services.Implementation
|
|||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync()
|
||||
public async Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync(int? accountId = null)
|
||||
{
|
||||
var data = await _workOrderDataService.GetAllAsync();
|
||||
return data.Where(w => w.istemplate != true).ToList();
|
||||
var data = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync(accountId);
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync()
|
||||
public async Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync(int? accountId = null)
|
||||
{
|
||||
var workOrders = await _workOrderDataService.GetAllWithDetailsAsync();
|
||||
var workOrders = await _workOrderDataService.GetAllWithDetailsAsync(accountId);
|
||||
return workOrders.Select(s => new WorkorderFilterVM
|
||||
{
|
||||
Id = s.Id,
|
||||
|
|
|
|||
|
|
@ -23,13 +23,15 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<(IEnumerable<WorkOrderListItemReadModel> Items, int TotalCount)> GetWorkOrderListPagedAsync(
|
||||
int page, int pageSize, string? search, string? status, string? assignee,
|
||||
int? locationId, string? sortBy, string? sortDir, int? accountId = null);
|
||||
Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id);
|
||||
Task<WorkOrderDetailReadModel?> GetWorkOrderDetailAsync(int id, int? accountId = null);
|
||||
Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrdersAsync(
|
||||
string[] assignto, int[]? location, string[]? priority,
|
||||
string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize);
|
||||
string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize,
|
||||
int? accountId = null);
|
||||
Task<FilteredWorkOrderPageDTO> GetFilteredWorkOrders2Async(
|
||||
string[] assignto, int[]? location, string[]? priority,
|
||||
string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize);
|
||||
string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize,
|
||||
int? accountId = null);
|
||||
Task<WorkOrderDTO?> ChangeStatusAsync(int id, string status, string userId);
|
||||
Task<ChangeAssignmentResult?> ChangeAssignmentAsync(int id, string? assignTo, string userId);
|
||||
Task<int> CreateWorkOrderWithDetailsAsync(CreateWorkOrderWithDetailsInput input, ClaimsPrincipal user, string userId);
|
||||
|
|
@ -39,7 +41,7 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<CommentResult> AddCommentJsonAsync(AddCommentInput input, string userId);
|
||||
Task<IEnumerable<CommentListItemReadModel>> GetCommentsAsync();
|
||||
Task<IEnumerable<CommentListItemReadModel>> GetCommentsByWorkorderIdAsync(int woid);
|
||||
Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync();
|
||||
Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync();
|
||||
Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync(int? accountId = null);
|
||||
Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync(int? accountId = null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,4 +301,132 @@ public class WorkOrderAccountScopeTests
|
|||
|
||||
Assert.Equal("Forbidden", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyList_ScopedUser_HidesOtherAccountAndNullAccountRows()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await SeedThreeAccountRowsAsync(context);
|
||||
|
||||
var data = new WorkOrderDataService(context);
|
||||
var scoped = (await data.GetAllWithDetailsAsync(accountId: 1)).ToList();
|
||||
var dd = await data.GetNonTemplateWorkOrdersWithLocationsAsync(accountId: 1);
|
||||
|
||||
Assert.Single(scoped);
|
||||
Assert.Equal(1, scoped[0].Id);
|
||||
Assert.Single(dd);
|
||||
Assert.Equal(1, dd[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyList_OrgWide_IncludesNullAccountRows()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await SeedThreeAccountRowsAsync(context);
|
||||
|
||||
var data = new WorkOrderDataService(context);
|
||||
var orgWide = (await data.GetAllWithDetailsAsync(accountId: null)).ToList();
|
||||
|
||||
Assert.Equal(3, orgWide.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyDetail_CrossAccount_ReturnsNull()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await SeedThreeAccountRowsAsync(context);
|
||||
|
||||
var data = new WorkOrderDataService(context);
|
||||
var detail = await data.GetWorkOrderDetailAsync(2, accountId: 1);
|
||||
|
||||
Assert.Null(detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyDetail_SameAccount_ReturnsRow()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await SeedThreeAccountRowsAsync(context);
|
||||
|
||||
var data = new WorkOrderDataService(context);
|
||||
var detail = await data.GetWorkOrderDetailAsync(1, accountId: 1);
|
||||
|
||||
Assert.NotNull(detail);
|
||||
Assert.Equal(1, detail!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyDetail_OrgWide_CanReadNullAccountRow()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await SeedThreeAccountRowsAsync(context);
|
||||
|
||||
var data = new WorkOrderDataService(context);
|
||||
var detail = await data.GetWorkOrderDetailAsync(3, accountId: null);
|
||||
|
||||
Assert.NotNull(detail);
|
||||
Assert.Equal(3, detail!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyFiltered_ScopedUser_HidesOtherAccountAndNullAccountRows()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await SeedThreeAccountRowsAsync(context);
|
||||
|
||||
var service = CreateLegacyReadService(context);
|
||||
var page = await service.GetFilteredWorkOrdersAsync(
|
||||
Array.Empty<string>(), null, null, null, null,
|
||||
search: "", sort: "", sortby: "", page: 1, pageSize: 50, accountId: 1);
|
||||
|
||||
Assert.Equal(1, page.TotalCount);
|
||||
Assert.Single(page.Data);
|
||||
Assert.Equal(1, page.Data.First().Id);
|
||||
}
|
||||
|
||||
private static WorkOrderService CreateLegacyReadService(ApplicationDbContext context)
|
||||
{
|
||||
return new WorkOrderService(
|
||||
new WorkOrderDataService(context),
|
||||
new CommentDataService(context),
|
||||
new UserDataService(context),
|
||||
new QuotesDataService(context),
|
||||
fileStorage: null!,
|
||||
new CreateWorkOrderValidation(),
|
||||
new UpdateWorkOrderValidation(),
|
||||
WorkOrderAccountTestHelpers.Resolver(context));
|
||||
}
|
||||
|
||||
private static async Task SeedThreeAccountRowsAsync(ApplicationDbContext context)
|
||||
{
|
||||
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 1, "A");
|
||||
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 2, "B");
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "00000000001",
|
||||
AccountId = 1,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
istemplate = false
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
InternalWONumber = "00000000002",
|
||||
AccountId = 2,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
istemplate = false
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 3,
|
||||
InternalWONumber = "00000000003",
|
||||
AccountId = null,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
istemplate = false
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue