Merge remote-tracking branch 'origin/dev' into fix/ab/sh-252-workorder-site-account

This commit is contained in:
Alexandre Brandizzi 2026-08-26 15:03:16 -03:00
commit 2f47798e67
11 changed files with 455 additions and 23 deletions

View file

@ -29,6 +29,7 @@ public class LocationControllerSitesTests
var dataService = new LocationDataService(ctx);
var service = new LocationService(
dataService,
new AccountDataService(ctx),
Mock.Of<ICreateLocationValidation>(),
Mock.Of<IUpdateLocationValidation>());

View file

@ -1,3 +1,4 @@
using System.Security.Claims;
using Api.SeaHavenIndustries.Controllers;
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
@ -87,14 +88,73 @@ public class LocationControllerTests
var response = ok.Value.Should().BeOfType<DataResponse>().Subject;
response.Status.Should().Be("200");
response.Message.Should().Be("Location Created Successfully");
service.Verify(s => s.CreateLocationFromRequestAsync(It.IsAny<LocationCreateRequestDTO>(), It.IsAny<CancellationToken>()), Times.Once);
service.Verify(s => s.CreateLocationFromRequestAsync(It.IsAny<LocationCreateRequestDTO>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task AddLocation_MapsAccountIdFromBodyString()
{
var service = new Mock<ILocationService>();
LocationCreateRequestDTO? captured = null;
service
.Setup(s => s.CreateLocationFromRequestAsync(It.IsAny<LocationCreateRequestDTO>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
.Callback<LocationCreateRequestDTO, ClaimsPrincipal, CancellationToken>((dto, _, _) => captured = dto)
.Returns(Task.CompletedTask);
var result = await NewController(service).AddLocation(
new Location_DTO { Name = "X", AccountId = "1" },
CancellationToken.None);
result.Should().BeOfType<OkObjectResult>();
captured.Should().NotBeNull();
captured!.AccountId.Should().Be(1);
}
[Fact]
public async Task AddLocation_InvalidAccountIdString_ReturnsValidationError()
{
var service = new Mock<ILocationService>();
var result = await NewController(service).AddLocation(
new Location_DTO { Name = "X", AccountId = "abc" },
CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
bad.Value.Should().BeOfType<Response>().Subject.Message.Should().Contain("accountId must be a valid integer.");
service.Verify(
s => s.CreateLocationFromRequestAsync(
It.IsAny<LocationCreateRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task EditLocation_InvalidAccountIdString_ReturnsValidationError()
{
var service = new Mock<ILocationService>();
var result = await NewController(service).EditLocation(
1,
new EditLocation_DTO { Name = "X", AccountId = "abc" },
CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
bad.Value.Should().BeOfType<Response>().Subject.Message.Should().Contain("accountId must be a valid integer.");
service.Verify(
s => s.UpdateLocationFromRequestAsync(
It.IsAny<int>(),
It.IsAny<LocationUpdateRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task EditLocation_WhenMissing_ReturnsNotFound()
{
var service = new Mock<ILocationService>();
service.Setup(s => s.UpdateLocationFromRequestAsync(99, It.IsAny<LocationUpdateRequestDTO>(), It.IsAny<CancellationToken>()))
service.Setup(s => s.UpdateLocationFromRequestAsync(99, It.IsAny<LocationUpdateRequestDTO>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new KeyNotFoundException());
var result = await NewController(service).EditLocation(99, new EditLocation_DTO { Name = "X" }, CancellationToken.None);

View file

@ -1,8 +1,12 @@
using System.Security.Claims;
using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Moq;
using SeaHaven.DataServices.Implementation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Implementation;
using SeaHaven.Services.Validation;
using Xunit;
@ -20,7 +24,17 @@ public class LocationServiceTests
}
private static LocationService NewService(ApplicationDbContext ctx) =>
new(new LocationDataService(ctx), new CreateLocationValidation(), new UpdateLocationValidation());
new(
new LocationDataService(ctx),
new AccountDataService(ctx),
new CreateLocationValidation(),
new UpdateLocationValidation());
private static void SeedAccount(ApplicationDbContext ctx, int id, string name = "Customer")
{
ctx.Accounts.Add(new Accounts { Id = id, Name = name, IsDeleted = false });
ctx.SaveChanges();
}
private static Locations SeedLocation(ApplicationDbContext ctx, string name, string? city = null, string? status = "Active")
{
@ -30,10 +44,43 @@ public class LocationServiceTests
return loc;
}
private static ClaimsPrincipal OrgWideAdmin()
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, "admin-1"),
new(ClaimTypes.Role, "Admin"),
new(SeaHavenClaimTypes.OrgScope, SeaHavenClaimTypes.OrgScopeAll)
};
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
}
private static ClaimsPrincipal AccountUser(int accountId, string role = "Dispatcher")
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, "actor-1"),
new(ClaimTypes.Role, role),
new(SeaHavenClaimTypes.AccountId, accountId.ToString())
};
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
}
private static ClaimsPrincipal MissingScope()
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, "actor-1"),
new(ClaimTypes.Role, "Dispatcher")
};
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
}
[Fact]
public async Task CreateLocationFromRequestAsync_PersistsMappedFields()
{
using var ctx = NewContext();
SeedAccount(ctx, 9);
var service = NewService(ctx);
await service.CreateLocationFromRequestAsync(new LocationCreateRequestDTO
@ -48,11 +95,11 @@ public class LocationServiceTests
ContactEmail = "wh@example.com",
Status = "Active",
AccountId = 9
}, CancellationToken.None);
}, OrgWideAdmin(), CancellationToken.None);
var entity = ctx.Locations.Single();
entity.Name.Should().Be("Warehouse");
entity.AccountId.Should().BeNull();
entity.AccountId.Should().Be(9);
entity.Title.Should().Be("Main WH");
entity.Address1.Should().Be("1 Depot Rd");
entity.City.Should().Be("Austin");
@ -107,7 +154,7 @@ public class LocationServiceTests
Address = "9 New St",
City = "Plano",
Status = "Inactive"
}, CancellationToken.None);
}, OrgWideAdmin(), CancellationToken.None);
var row = ctx.Locations.Single();
row.Name.Should().Be("New");
@ -115,7 +162,7 @@ public class LocationServiceTests
row.City.Should().Be("Plano");
row.Status.Should().Be("Inactive");
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(existing.Id + 999, new LocationUpdateRequestDTO { Name = "X" }, CancellationToken.None);
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(existing.Id + 999, new LocationUpdateRequestDTO { Name = "X" }, OrgWideAdmin(), CancellationToken.None);
await act.Should().ThrowAsync<KeyNotFoundException>();
}
@ -131,15 +178,92 @@ public class LocationServiceTests
{
Name = "New",
City = "Plano"
}, CancellationToken.None);
}, OrgWideAdmin(), CancellationToken.None);
ctx.Locations.Single().AccountId.Should().Be(4);
}
[Fact]
public async Task UpdateLocationFromRequestAsync_IgnoresClientAccountIdRelabel()
public async Task UpdateLocationFromRequestAsync_AccountScopedCaller_CannotUpdateForeignLocationWhenAccountIdOmitted()
{
using var ctx = NewContext();
var existing = SeedLocation(ctx, "Foreign", "Dallas");
existing.AccountId = 99;
await ctx.SaveChangesAsync();
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(
existing.Id,
new LocationUpdateRequestDTO { Name = "Hijacked", City = "Austin" },
AccountUser(4),
CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedAccessException>();
var row = ctx.Locations.Single();
row.Name.Should().Be("Foreign");
row.City.Should().Be("Dallas");
row.AccountId.Should().Be(99);
}
[Fact]
public async Task UpdateLocationFromRequestAsync_AccountScopedCaller_CanUpdateOwnLocationWhenAccountIdOmitted()
{
using var ctx = NewContext();
var existing = SeedLocation(ctx, "Owned", "Dallas");
existing.AccountId = 4;
await ctx.SaveChangesAsync();
await NewService(ctx).UpdateLocationFromRequestAsync(
existing.Id,
new LocationUpdateRequestDTO { Name = "Renamed", City = "Austin" },
AccountUser(4),
CancellationToken.None);
var row = ctx.Locations.Single();
row.Name.Should().Be("Renamed");
row.City.Should().Be("Austin");
row.AccountId.Should().Be(4);
}
[Fact]
public async Task UpdateLocationFromRequestAsync_AccountScopedCaller_CannotUpdateOrphanWhenAccountIdOmitted()
{
using var ctx = NewContext();
var existing = SeedLocation(ctx, "Orphan", "Dallas");
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(
existing.Id,
new LocationUpdateRequestDTO { Name = "Claimed", City = "Austin" },
AccountUser(4),
CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedAccessException>();
ctx.Locations.Single().Name.Should().Be("Orphan");
}
[Fact]
public async Task UpdateLocationFromRequestAsync_MissingScope_CannotUpdate()
{
using var ctx = NewContext();
var existing = SeedLocation(ctx, "Owned", "Dallas");
existing.AccountId = 4;
await ctx.SaveChangesAsync();
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(
existing.Id,
new LocationUpdateRequestDTO { Name = "Renamed" },
MissingScope(),
CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedAccessException>();
ctx.Locations.Single().Name.Should().Be("Owned");
}
[Fact]
public async Task UpdateLocationFromRequestAsync_AppliesClientAccountIdWhenProvided()
{
using var ctx = NewContext();
SeedAccount(ctx, 4);
SeedAccount(ctx, 99);
var existing = SeedLocation(ctx, "Owned", "Austin");
existing.AccountId = 4;
await ctx.SaveChangesAsync();
@ -148,11 +272,136 @@ public class LocationServiceTests
{
Name = "Owned",
AccountId = 99
}, CancellationToken.None);
}, OrgWideAdmin(), CancellationToken.None);
ctx.Locations.Single().AccountId.Should().Be(99);
}
[Fact]
public async Task UpdateLocationFromRequestAsync_UnknownAccount_ThrowsValidationException()
{
using var ctx = NewContext();
var existing = SeedLocation(ctx, "Owned", "Austin");
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(
existing.Id,
new LocationUpdateRequestDTO { Name = "Owned", AccountId = 404 },
OrgWideAdmin(),
CancellationToken.None);
await act.Should().ThrowAsync<FluentValidation.ValidationException>();
ctx.Locations.Single().AccountId.Should().BeNull();
}
[Fact]
public async Task CreateLocationFromRequestAsync_UnknownAccount_ThrowsValidationException()
{
using var ctx = NewContext();
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
new LocationCreateRequestDTO { Name = "Warehouse", AccountId = 404 },
OrgWideAdmin(),
CancellationToken.None);
await act.Should().ThrowAsync<FluentValidation.ValidationException>();
ctx.Locations.Should().BeEmpty();
}
[Fact]
public async Task CreateLocationFromRequestAsync_SoftDeletedAccount_ThrowsValidationException()
{
using var ctx = NewContext();
ctx.Accounts.Add(new Accounts { Id = 9, Name = "Gone", IsDeleted = true });
ctx.SaveChanges();
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
new LocationCreateRequestDTO { Name = "Warehouse", AccountId = 9 },
OrgWideAdmin(),
CancellationToken.None);
await act.Should().ThrowAsync<FluentValidation.ValidationException>();
ctx.Locations.Should().BeEmpty();
}
[Fact]
public async Task CreateLocationFromRequestAsync_AccountScopedCaller_CannotAssignOtherAccount()
{
using var ctx = NewContext();
SeedAccount(ctx, 4);
SeedAccount(ctx, 99);
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
new LocationCreateRequestDTO { Name = "Site", AccountId = 99 },
AccountUser(4),
CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedAccessException>();
ctx.Locations.Should().BeEmpty();
}
[Fact]
public async Task UpdateLocationFromRequestAsync_AccountScopedCaller_CannotStealOtherAccountLocation()
{
using var ctx = NewContext();
SeedAccount(ctx, 4);
SeedAccount(ctx, 99);
var existing = SeedLocation(ctx, "Owned", "Austin");
existing.AccountId = 4;
await ctx.SaveChangesAsync();
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(
existing.Id,
new LocationUpdateRequestDTO { Name = "Owned", AccountId = 99 },
AccountUser(99),
CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedAccessException>();
ctx.Locations.Single().AccountId.Should().Be(4);
}
[Fact]
public async Task CreateLocationFromRequestAsync_MissingScope_CannotAssignAccount()
{
using var ctx = NewContext();
SeedAccount(ctx, 9);
var act = () => NewService(ctx).CreateLocationFromRequestAsync(
new LocationCreateRequestDTO { Name = "Site", AccountId = 9 },
MissingScope(),
CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedAccessException>();
ctx.Locations.Should().BeEmpty();
}
[Fact]
public async Task CreateLocationFromRequestAsync_ForwardsCancellationToAccountLookup()
{
using var ctx = NewContext();
var accounts = new Mock<IAccountDataService>();
CancellationToken seen = default;
accounts
.Setup(a => a.ExistsActiveAsync(9, It.IsAny<CancellationToken>()))
.Callback<int, CancellationToken>((_, token) => seen = token)
.ReturnsAsync(true);
var service = new LocationService(
new LocationDataService(ctx),
accounts.Object,
new CreateLocationValidation(),
new UpdateLocationValidation());
using var cts = new CancellationTokenSource();
await service.CreateLocationFromRequestAsync(
new LocationCreateRequestDTO { Name = "Site", AccountId = 9 },
OrgWideAdmin(),
cts.Token);
seen.Should().Be(cts.Token);
accounts.Verify(a => a.ExistsActiveAsync(9, cts.Token), Times.Once);
}
[Fact]
public async Task CreateLocationAsync_IgnoresClientAccountId()
{

View file

@ -3,6 +3,7 @@ using Api.SeaHavenIndustries.Helper;
using Data.SeaHavenIndustries;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SeaHaven.Services.DTOs;
@ -103,7 +104,7 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
await _locationService.CreateLocationFromRequestAsync(MapToCreateRequest(model), cancellationToken);
await _locationService.CreateLocationFromRequestAsync(MapToCreateRequest(model), User, cancellationToken);
return Ok(new DataResponse { Message = "Location Created Successfully", Status = "200" });
}
catch (ValidationException vex)
@ -111,6 +112,10 @@ namespace Api.SeaHavenIndustries.Controllers
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (UnauthorizedAccessException)
{
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = "You are not allowed to assign this account." });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = _logger.Sanitize(ex) });
@ -122,7 +127,7 @@ namespace Api.SeaHavenIndustries.Controllers
{
try
{
await _locationService.UpdateLocationFromRequestAsync(id, MapToUpdateRequest(model), cancellationToken);
await _locationService.UpdateLocationFromRequestAsync(id, MapToUpdateRequest(model), User, cancellationToken);
return Ok(new DataResponse { Message = "Location Updated Successfully", Status = "200" });
}
catch (ValidationException vex)
@ -130,6 +135,10 @@ namespace Api.SeaHavenIndustries.Controllers
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (UnauthorizedAccessException)
{
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = "You are not allowed to assign this account." });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Location not found" });
@ -179,7 +188,8 @@ namespace Api.SeaHavenIndustries.Controllers
ZipCode = model.ZipCode,
Phone = model.Phone,
ContactEmail = model.ContactEmail,
Status = model.Status
Status = model.Status,
AccountId = model.GetAccountId()
};
}
@ -195,7 +205,8 @@ namespace Api.SeaHavenIndustries.Controllers
ZipCode = model.ZipCode,
Phone = model.Phone,
ContactEmail = model.ContactEmail,
Status = model.Status
Status = model.Status,
AccountId = model.GetAccountId()
};
}
}

View file

@ -17,8 +17,7 @@ namespace Api.SeaHavenIndustries.DTOs
public string? Status { get; set; }
public string? AccountId { get; set; }
public int? GetAccountId() =>
int.TryParse(AccountId, out var id) ? id : null;
public int? GetAccountId() => LocationAccountIdMapping.ParseOptional(AccountId);
// ✅ Map API DTO to Service DTO
public UpdateLocationDTO ToServiceUpdateDTO()

View file

@ -0,0 +1,25 @@
using System.Globalization;
using FluentValidation;
using FluentValidation.Results;
namespace Api.SeaHavenIndustries.DTOs
{
internal static class LocationAccountIdMapping
{
public static int? ParseOptional(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
if (!int.TryParse(raw.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var id))
{
throw new ValidationException(new[]
{
new ValidationFailure("AccountId", "accountId must be a valid integer.")
});
}
return id;
}
}
}

View file

@ -17,8 +17,7 @@ namespace Api.SeaHavenIndustries.DTOs
public string? Status { get; set; }
public string? AccountId { get; set; }
public int? GetAccountId() =>
int.TryParse(AccountId, out var id) ? id : null;
public int? GetAccountId() => LocationAccountIdMapping.ParseOptional(AccountId);
// ✅ Map API DTO to Service DTO
public CreateLocationDTO ToServiceCreateDTO()

View file

@ -92,6 +92,15 @@ namespace SeaHaven.DataServices.Implementation
return await _context.Accounts.AnyAsync(a => a.Id == id);
}
public async Task<bool> ExistsActiveAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.Accounts
.AsNoTracking()
.AnyAsync(
a => a.Id == id && (a.IsDeleted == false || a.IsDeleted == null),
cancellationToken);
}
public async Task<int> CountAsync()
{
return await _context.Accounts.CountAsync();

View file

@ -12,6 +12,7 @@ namespace SeaHaven.DataServices.Interfaces
Task UpdateAsync(Accounts account);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<bool> ExistsActiveAsync(int id, CancellationToken cancellationToken = default);
Task<int> CountAsync();
/// <summary>

View file

@ -1,7 +1,10 @@
using System.Security.Claims;
using Data.SeaHavenIndustries;
using FluentValidation;
using FluentValidation.Results;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.Validation;
@ -14,15 +17,18 @@ namespace SeaHaven.Services.Implementation
public class LocationService : ILocationService
{
private readonly ILocationDataService _locationDataService;
private readonly IAccountDataService _accountDataService;
private readonly ICreateLocationValidation _createValidator;
private readonly IUpdateLocationValidation _updateValidator;
public LocationService(
ILocationDataService locationDataService,
IAccountDataService accountDataService,
ICreateLocationValidation createValidator,
IUpdateLocationValidation updateValidator)
{
_locationDataService = locationDataService;
_accountDataService = accountDataService;
_createValidator = createValidator;
_updateValidator = updateValidator;
}
@ -177,8 +183,13 @@ namespace SeaHaven.Services.Implementation
return location == null ? null : MapToDTO(location);
}
public async Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken)
public async Task CreateLocationFromRequestAsync(
LocationCreateRequestDTO request,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
await EnsureAccountAssignableAsync(user, request.AccountId, existingLocationAccountId: null, cancellationToken);
var location = new Locations
{
Name = request.Name,
@ -189,18 +200,25 @@ namespace SeaHaven.Services.Implementation
Zip = request.ZipCode,
PhoneNumber = request.Phone,
Email = request.ContactEmail,
Status = request.Status
Status = request.Status,
AccountId = request.AccountId
};
await _locationDataService.AddAsync(location, cancellationToken);
}
public async Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, CancellationToken cancellationToken)
public async Task UpdateLocationFromRequestAsync(
int id,
LocationUpdateRequestDTO request,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
var location = await _locationDataService.GetByIdForUpdateAsync(id, cancellationToken);
if (location == null)
throw new KeyNotFoundException($"Location with ID {id} not found");
EnsureLocationInCallerScope(user, location.AccountId);
location.Name = request.Name;
location.Title = request.Title;
location.Address1 = request.Address;
@ -211,6 +229,12 @@ namespace SeaHaven.Services.Implementation
location.Email = request.ContactEmail;
location.Status = request.Status;
if (request.AccountId is int accountId)
{
await EnsureAccountAssignableAsync(user, accountId, location.AccountId, cancellationToken);
location.AccountId = accountId;
}
await _locationDataService.UpdateAsync(location, cancellationToken);
}
@ -219,6 +243,59 @@ namespace SeaHaven.Services.Implementation
return _locationDataService.DeleteByIdAsync(id, cancellationToken);
}
private static void EnsureLocationInCallerScope(ClaimsPrincipal user, int? locationAccountId)
{
switch (WorkOrderMediaAuthorization.ResolveMediaScope(user))
{
case MediaAccountScope.OrgWide:
return;
case MediaAccountScope.Account caller:
if (locationAccountId != caller.AccountId)
throw new UnauthorizedAccessException();
return;
default:
throw new UnauthorizedAccessException();
}
}
private async Task EnsureAccountAssignableAsync(
ClaimsPrincipal user,
int? requestedAccountId,
int? existingLocationAccountId,
CancellationToken cancellationToken)
{
if (requestedAccountId is not int accountId)
return;
if (!await _accountDataService.ExistsActiveAsync(accountId, cancellationToken))
{
throw new ValidationException(new[]
{
new ValidationFailure(nameof(LocationCreateRequestDTO.AccountId), "Account was not found.")
});
}
switch (WorkOrderMediaAuthorization.ResolveMediaScope(user))
{
case MediaAccountScope.OrgWide:
return;
case MediaAccountScope.Account caller:
if (accountId != caller.AccountId
|| (existingLocationAccountId is int current && current != caller.AccountId))
{
throw new UnauthorizedAccessException();
}
return;
default:
throw new UnauthorizedAccessException();
}
}
// Private helper methods
private LocationDTO MapToDTO(Locations location)
{

View file

@ -1,3 +1,4 @@
using System.Security.Claims;
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Interfaces
@ -20,8 +21,8 @@ namespace SeaHaven.Services.Interfaces
Task<PagedResult<LocationDTO>> GetLocationListPagedAsync(int page, int pageSize, string? search, CancellationToken cancellationToken);
Task<LocationDTO?> GetLocationDetailAsync(int id, CancellationToken cancellationToken);
Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, CancellationToken cancellationToken);
Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, CancellationToken cancellationToken);
Task CreateLocationFromRequestAsync(LocationCreateRequestDTO request, ClaimsPrincipal user, CancellationToken cancellationToken);
Task UpdateLocationFromRequestAsync(int id, LocationUpdateRequestDTO request, ClaimsPrincipal user, CancellationToken cancellationToken);
Task<bool> DeleteLocationByIdAsync(int id, CancellationToken cancellationToken);
}
}