mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-04 19:32:10 +00:00
feat(team-members): add member detail editing (SH-326)
This commit is contained in:
parent
8a00476d38
commit
b9c916cb22
8 changed files with 344 additions and 13 deletions
|
|
@ -9,6 +9,7 @@ using Moq;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Api.SeaHavenIndustries.Tests;
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
@ -24,7 +25,9 @@ public sealed class TeamMemberServiceTests
|
||||||
userManager.Object,
|
userManager.Object,
|
||||||
roleManager.Object,
|
roleManager.Object,
|
||||||
Mock.Of<IUserServiceAreaDataService>(),
|
Mock.Of<IUserServiceAreaDataService>(),
|
||||||
Mock.Of<ITeamPermissionOverrideDataService>());
|
Mock.Of<ITeamPermissionOverrideDataService>(),
|
||||||
|
Mock.Of<IUserDataService>(),
|
||||||
|
Mock.Of<ITeamPermissionService>());
|
||||||
|
|
||||||
var result = await service.CreateAsync(
|
var result = await service.CreateAsync(
|
||||||
ValidRequest() with { ServiceAreas = Array.Empty<string>() },
|
ValidRequest() with { ServiceAreas = Array.Empty<string>() },
|
||||||
|
|
@ -39,7 +42,7 @@ public sealed class TeamMemberServiceTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_RejectsUnknownColorAndPermission()
|
public async Task Create_RejectsUnknownColorAndPermission()
|
||||||
{
|
{
|
||||||
var service = NewService(out var userManager, out _, out _, out _);
|
var service = NewService(out var userManager, out _, out _, out _, out _, out _);
|
||||||
|
|
||||||
var badColor = await service.CreateAsync(
|
var badColor = await service.CreateAsync(
|
||||||
ValidRequest() with { Color = "#000000" },
|
ValidRequest() with { Color = "#000000" },
|
||||||
|
|
@ -64,7 +67,7 @@ public sealed class TeamMemberServiceTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_PersistsPendingUserWithoutPasswordAndStoresAreasAndOverrides()
|
public async Task Create_PersistsPendingUserWithoutPasswordAndStoresAreasAndOverrides()
|
||||||
{
|
{
|
||||||
var service = NewService(out var userManager, out var roleManager, out var areas, out var overrides);
|
var service = NewService(out var userManager, out var roleManager, out var areas, out var overrides, out _, out _);
|
||||||
ApplicationUser? created = null;
|
ApplicationUser? created = null;
|
||||||
userManager
|
userManager
|
||||||
.Setup(manager => manager.CreateAsync(It.IsAny<ApplicationUser>()))
|
.Setup(manager => manager.CreateAsync(It.IsAny<ApplicationUser>()))
|
||||||
|
|
@ -95,17 +98,136 @@ public sealed class TeamMemberServiceTests
|
||||||
overrides.Verify(data => data.SetOverridesAsync("new-user", It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["deleteSites"] == UserPermissionState.Allow), It.IsAny<CancellationToken>()), Times.Once);
|
overrides.Verify(data => data.SetOverridesAsync("new-user", It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["deleteSites"] == UserPermissionState.Allow), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Update_ProtectsAccountOwnerFromRoleChangeAndDeactivation()
|
||||||
|
{
|
||||||
|
var service = NewService(
|
||||||
|
out var userManager,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out var userData,
|
||||||
|
out _);
|
||||||
|
var owner = new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "owner",
|
||||||
|
FirstName = "Primary",
|
||||||
|
Email = "owner@example.com",
|
||||||
|
IsAccountOwner = true
|
||||||
|
};
|
||||||
|
userManager.Setup(manager => manager.FindByIdAsync(owner.Id)).ReturnsAsync(owner);
|
||||||
|
userManager.Setup(manager => manager.GetRolesAsync(owner)).ReturnsAsync(new List<string> { "Admin" });
|
||||||
|
userData.Setup(data => data.IsAccountOwnerAsync(owner.Id, It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
||||||
|
|
||||||
|
var result = await service.UpdateAsync(
|
||||||
|
owner.Id,
|
||||||
|
new UpdateTeamMemberRequestDTO
|
||||||
|
{
|
||||||
|
Name = "Primary",
|
||||||
|
Role = "Dispatcher",
|
||||||
|
Color = "#F59E0B",
|
||||||
|
Email = owner.Email,
|
||||||
|
ServiceAreas = new[] { "East" },
|
||||||
|
IsActive = false
|
||||||
|
},
|
||||||
|
Admin(),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.Success.Should().BeFalse();
|
||||||
|
result.Error.Should().Be("Forbidden");
|
||||||
|
userData.Verify(data => data.UpdateUserAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Update_ReplacesAreasAndOverridesAndReturnsFullDetail()
|
||||||
|
{
|
||||||
|
var service = NewService(
|
||||||
|
out var userManager,
|
||||||
|
out var roleManager,
|
||||||
|
out var areas,
|
||||||
|
out var overrides,
|
||||||
|
out var userData,
|
||||||
|
out var permissions);
|
||||||
|
var user = new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "dispatcher",
|
||||||
|
FirstName = "Taylor",
|
||||||
|
Email = "old@example.com",
|
||||||
|
Color = "#F59E0B",
|
||||||
|
UniqueName = "Active"
|
||||||
|
};
|
||||||
|
userManager.Setup(manager => manager.FindByIdAsync(user.Id)).ReturnsAsync(user);
|
||||||
|
userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(new List<string> { "Dispatcher" });
|
||||||
|
userManager.Setup(manager => manager.FindByEmailAsync("new@example.com")).ReturnsAsync((ApplicationUser?)null);
|
||||||
|
roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true);
|
||||||
|
areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new[] { "Central" });
|
||||||
|
permissions.Setup(data => data.GetProfileAsync(user.Id, It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(TeamPermissionResult<TeamPermissionProfileDTO>.Success(new TeamPermissionProfileDTO
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
RoleName = "Dispatcher",
|
||||||
|
Permissions = new[]
|
||||||
|
{
|
||||||
|
new TeamPermissionValueDTO
|
||||||
|
{
|
||||||
|
PermissionKey = "createSites",
|
||||||
|
OverrideState = UserPermissionState.Deny,
|
||||||
|
IsGranted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
var result = await service.UpdateAsync(
|
||||||
|
user.Id,
|
||||||
|
new UpdateTeamMemberRequestDTO
|
||||||
|
{
|
||||||
|
Name = "Taylor Updated",
|
||||||
|
Role = "Dispatcher",
|
||||||
|
Color = "#0D9488",
|
||||||
|
Email = "new@example.com",
|
||||||
|
Phone = "555-0101",
|
||||||
|
ServiceAreas = new[] { "West" },
|
||||||
|
PermissionOverrides = new Dictionary<string, UserPermissionState>
|
||||||
|
{
|
||||||
|
["createSites"] = UserPermissionState.Deny
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Admin(),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
result.Member!.Name.Should().Be("Taylor Updated");
|
||||||
|
result.Member.Email.Should().Be("new@example.com");
|
||||||
|
result.Member.ServiceAreas.Should().Equal("Central");
|
||||||
|
result.Member.Permissions.Should().ContainSingle(permission => permission.PermissionKey == "createSites");
|
||||||
|
userData.Verify(data => data.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
areas.Verify(data => data.ReplaceAsync(user.Id, It.Is<IReadOnlyCollection<string>>(value => value.SequenceEqual(new[] { "West" })), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
overrides.Verify(data => data.ClearOverridesAsync(user.Id, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
overrides.Verify(data => data.SetOverridesAsync(user.Id, It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["createSites"] == UserPermissionState.Deny), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
private static TeamMemberService NewService(
|
private static TeamMemberService NewService(
|
||||||
out Mock<UserManager<ApplicationUser>> userManager,
|
out Mock<UserManager<ApplicationUser>> userManager,
|
||||||
out Mock<RoleManager<IdentityRole>> roleManager,
|
out Mock<RoleManager<IdentityRole>> roleManager,
|
||||||
out Mock<IUserServiceAreaDataService> areas,
|
out Mock<IUserServiceAreaDataService> areas,
|
||||||
out Mock<ITeamPermissionOverrideDataService> overrides)
|
out Mock<ITeamPermissionOverrideDataService> overrides,
|
||||||
|
out Mock<IUserDataService> userData,
|
||||||
|
out Mock<ITeamPermissionService> permissions)
|
||||||
{
|
{
|
||||||
userManager = UserManager();
|
userManager = UserManager();
|
||||||
roleManager = RoleManager();
|
roleManager = RoleManager();
|
||||||
areas = new Mock<IUserServiceAreaDataService>();
|
areas = new Mock<IUserServiceAreaDataService>();
|
||||||
overrides = new Mock<ITeamPermissionOverrideDataService>();
|
overrides = new Mock<ITeamPermissionOverrideDataService>();
|
||||||
return new TeamMemberService(userManager.Object, roleManager.Object, areas.Object, overrides.Object);
|
userData = new Mock<IUserDataService>();
|
||||||
|
permissions = new Mock<ITeamPermissionService>();
|
||||||
|
return new TeamMemberService(
|
||||||
|
userManager.Object,
|
||||||
|
roleManager.Object,
|
||||||
|
areas.Object,
|
||||||
|
overrides.Object,
|
||||||
|
userData.Object,
|
||||||
|
permissions.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Mock<UserManager<ApplicationUser>> UserManager()
|
private static Mock<UserManager<ApplicationUser>> UserManager()
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,35 @@ public sealed class TeamMemberController : ControllerBase
|
||||||
|
|
||||||
return Ok(outcome.Member);
|
return Ok(outcome.Member);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{userId}")]
|
||||||
|
public async Task<IActionResult> Get(string userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var outcome = await _teamMemberService.GetAsync(userId, User, cancellationToken);
|
||||||
|
return ToActionResult(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{userId}")]
|
||||||
|
public async Task<IActionResult> Update(
|
||||||
|
string userId,
|
||||||
|
UpdateTeamMemberRequestDTO request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var outcome = await _teamMemberService.UpdateAsync(userId, request, User, cancellationToken);
|
||||||
|
return ToActionResult(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult ToActionResult(TeamMemberOperationOutcomeDTO outcome)
|
||||||
|
{
|
||||||
|
if (outcome.Success)
|
||||||
|
return Ok(outcome.Member);
|
||||||
|
|
||||||
|
if (string.Equals(outcome.Error, "Forbidden", StringComparison.Ordinal))
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
if (string.Equals(outcome.Error, "Team member not found.", StringComparison.Ordinal))
|
||||||
|
return NotFound(new { message = outcome.Error });
|
||||||
|
|
||||||
|
return BadRequest(new { message = outcome.Error });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,4 +113,11 @@ public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideD
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ClearOverridesAsync(string userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _context.UserPermissionOverrides
|
||||||
|
.Where(item => item.UserId == userId)
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,6 @@ public interface ITeamPermissionOverrideDataService
|
||||||
string userId,
|
string userId,
|
||||||
IReadOnlyDictionary<string, UserPermissionState> overrides,
|
IReadOnlyDictionary<string, UserPermissionState> overrides,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task ClearOverridesAsync(string userId, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,5 +140,11 @@ public sealed class TeamPermissionServiceTests
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task ClearOverridesAsync(string userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_overrides.Clear();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ public sealed record CreateTeamMemberRequestDTO
|
||||||
public IReadOnlyDictionary<string, UserPermissionState>? PermissionOverrides { get; init; }
|
public IReadOnlyDictionary<string, UserPermissionState>? PermissionOverrides { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TeamMemberCreatedDTO
|
public class TeamMemberCreatedDTO
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
|
|
@ -25,6 +25,32 @@ public sealed class TeamMemberCreatedDTO
|
||||||
public bool PendingRegistration { get; init; }
|
public bool PendingRegistration { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class TeamMemberDetailDTO : TeamMemberCreatedDTO
|
||||||
|
{
|
||||||
|
public string Status { get; init; } = "Active";
|
||||||
|
public bool IsAccountOwner { get; init; }
|
||||||
|
public IReadOnlyList<TeamPermissionValueDTO> Permissions { get; init; } = Array.Empty<TeamPermissionValueDTO>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record UpdateTeamMemberRequestDTO
|
||||||
|
{
|
||||||
|
public string? Name { get; init; }
|
||||||
|
public string? Role { get; init; }
|
||||||
|
public string? Color { get; init; }
|
||||||
|
public string? Email { get; init; }
|
||||||
|
public string? Phone { get; init; }
|
||||||
|
public IReadOnlyList<string>? ServiceAreas { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, UserPermissionState>? PermissionOverrides { get; init; }
|
||||||
|
public bool IsActive { get; init; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TeamMemberOperationOutcomeDTO
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public string? Error { get; init; }
|
||||||
|
public TeamMemberDetailDTO? Member { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class CreateTeamMemberOutcomeDTO
|
public sealed class CreateTeamMemberOutcomeDTO
|
||||||
{
|
{
|
||||||
public bool Success { get; init; }
|
public bool Success { get; init; }
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,23 @@ public sealed class TeamMemberService : ITeamMemberService
|
||||||
private readonly RoleManager<IdentityRole> _roleManager;
|
private readonly RoleManager<IdentityRole> _roleManager;
|
||||||
private readonly IUserServiceAreaDataService _areaDataService;
|
private readonly IUserServiceAreaDataService _areaDataService;
|
||||||
private readonly ITeamPermissionOverrideDataService _permissionDataService;
|
private readonly ITeamPermissionOverrideDataService _permissionDataService;
|
||||||
|
private readonly IUserDataService _userDataService;
|
||||||
|
private readonly ITeamPermissionService _permissionService;
|
||||||
|
|
||||||
public TeamMemberService(
|
public TeamMemberService(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
RoleManager<IdentityRole> roleManager,
|
RoleManager<IdentityRole> roleManager,
|
||||||
IUserServiceAreaDataService areaDataService,
|
IUserServiceAreaDataService areaDataService,
|
||||||
ITeamPermissionOverrideDataService permissionDataService)
|
ITeamPermissionOverrideDataService permissionDataService,
|
||||||
|
IUserDataService userDataService,
|
||||||
|
ITeamPermissionService permissionService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_roleManager = roleManager;
|
_roleManager = roleManager;
|
||||||
_areaDataService = areaDataService;
|
_areaDataService = areaDataService;
|
||||||
_permissionDataService = permissionDataService;
|
_permissionDataService = permissionDataService;
|
||||||
|
_userDataService = userDataService;
|
||||||
|
_permissionService = permissionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CreateTeamMemberOutcomeDTO> CreateAsync(
|
public async Task<CreateTeamMemberOutcomeDTO> CreateAsync(
|
||||||
|
|
@ -45,12 +51,9 @@ public sealed class TeamMemberService : ITeamMemberService
|
||||||
if (await _userManager.FindByEmailAsync(email!) is not null)
|
if (await _userManager.FindByEmailAsync(email!) is not null)
|
||||||
return Failure("Email is already in use.");
|
return Failure("Email is already in use.");
|
||||||
|
|
||||||
if (!await _roleManager.RoleExistsAsync(role!))
|
var roleError = await EnsureRoleAsync(role!);
|
||||||
{
|
if (roleError is not null)
|
||||||
var roleResult = await _roleManager.CreateAsync(new IdentityRole(role!));
|
return Failure(roleError);
|
||||||
if (!roleResult.Succeeded)
|
|
||||||
return Failure(roleResult.Errors.FirstOrDefault()?.Description ?? "Unable to create role.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var user = new ApplicationUser
|
var user = new ApplicationUser
|
||||||
|
|
@ -107,6 +110,128 @@ public sealed class TeamMemberService : ITeamMemberService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<TeamMemberOperationOutcomeDTO> GetAsync(
|
||||||
|
string userId,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (caller?.IsInRole("Admin") != true)
|
||||||
|
return OperationFailure("Forbidden");
|
||||||
|
|
||||||
|
var user = await _userManager.FindByIdAsync(userId);
|
||||||
|
if (user is null)
|
||||||
|
return OperationFailure("Team member not found.");
|
||||||
|
|
||||||
|
return await OperationSuccessAsync(user, caller, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TeamMemberOperationOutcomeDTO> UpdateAsync(
|
||||||
|
string userId,
|
||||||
|
UpdateTeamMemberRequestDTO request,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (caller?.IsInRole("Admin") != true)
|
||||||
|
return OperationFailure("Forbidden");
|
||||||
|
|
||||||
|
var user = await _userManager.FindByIdAsync(userId);
|
||||||
|
if (user is null)
|
||||||
|
return OperationFailure("Team member not found.");
|
||||||
|
|
||||||
|
var existingRoles = await _userManager.GetRolesAsync(user);
|
||||||
|
var existingRole = existingRoles.FirstOrDefault();
|
||||||
|
var candidate = new CreateTeamMemberRequestDTO
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
Role = request.Role,
|
||||||
|
Color = request.Color,
|
||||||
|
Email = request.Email,
|
||||||
|
Phone = request.Phone,
|
||||||
|
ServiceAreas = request.ServiceAreas,
|
||||||
|
PermissionOverrides = request.PermissionOverrides
|
||||||
|
};
|
||||||
|
var validation = Validate(candidate, out var role, out var color, out var email, out var areas, out var overrides);
|
||||||
|
if (validation is not null)
|
||||||
|
return OperationFailure(validation);
|
||||||
|
|
||||||
|
var roleChanged = !string.Equals(existingRole, role, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isOwner = await _userDataService.IsAccountOwnerAsync(user.Id, cancellationToken);
|
||||||
|
if (isOwner && (roleChanged || !request.IsActive))
|
||||||
|
return OperationFailure("Forbidden");
|
||||||
|
|
||||||
|
var duplicate = await _userManager.FindByEmailAsync(email!);
|
||||||
|
if (duplicate is not null && !string.Equals(duplicate.Id, user.Id, StringComparison.Ordinal))
|
||||||
|
return OperationFailure("Email is already in use.");
|
||||||
|
|
||||||
|
var roleError = await EnsureRoleAsync(role!);
|
||||||
|
if (roleError is not null)
|
||||||
|
return OperationFailure(roleError);
|
||||||
|
|
||||||
|
user.FirstName = candidate.Name!.Trim();
|
||||||
|
user.Email = email;
|
||||||
|
user.UserName = email;
|
||||||
|
user.NormalizedEmail = email!.ToUpperInvariant();
|
||||||
|
user.NormalizedUserName = email.ToUpperInvariant();
|
||||||
|
user.Contact = candidate.Phone?.Trim();
|
||||||
|
user.PhoneNumber = candidate.Phone?.Trim();
|
||||||
|
user.Color = color;
|
||||||
|
user.UniqueName = request.IsActive ? ActiveStatus : "Inactive";
|
||||||
|
user.IsDeleted = !request.IsActive;
|
||||||
|
|
||||||
|
if (roleChanged)
|
||||||
|
{
|
||||||
|
if (existingRoles.Count > 0)
|
||||||
|
await _userManager.RemoveFromRolesAsync(user, existingRoles);
|
||||||
|
await _userManager.AddToRoleAsync(user, role!);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userDataService.UpdateUserAsync(user, cancellationToken);
|
||||||
|
await _areaDataService.ReplaceAsync(user.Id, areas!, cancellationToken);
|
||||||
|
await _permissionDataService.ClearOverridesAsync(user.Id, cancellationToken);
|
||||||
|
await _permissionDataService.SetOverridesAsync(user.Id, overrides!, cancellationToken);
|
||||||
|
|
||||||
|
return await OperationSuccessAsync(user, caller, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> EnsureRoleAsync(string role)
|
||||||
|
{
|
||||||
|
if (await _roleManager.RoleExistsAsync(role))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var roleResult = await _roleManager.CreateAsync(new IdentityRole(role));
|
||||||
|
return roleResult.Succeeded
|
||||||
|
? null
|
||||||
|
: roleResult.Errors.FirstOrDefault()?.Description ?? "Unable to create role.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TeamMemberOperationOutcomeDTO> OperationSuccessAsync(
|
||||||
|
ApplicationUser user,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
|
var profile = await _permissionService.GetProfileAsync(user.Id, caller, cancellationToken);
|
||||||
|
var areas = await _areaDataService.GetForUserAsync(user.Id, cancellationToken);
|
||||||
|
return new TeamMemberOperationOutcomeDTO
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Member = new TeamMemberDetailDTO
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Name = $"{user.FirstName ?? ""} {user.LastName ?? ""}".Trim(),
|
||||||
|
Role = TeamMemberConstants.CanonicalRole(roles.FirstOrDefault()) ?? roles.FirstOrDefault() ?? "",
|
||||||
|
Color = user.Color ?? "",
|
||||||
|
Email = user.Email,
|
||||||
|
Phone = user.Contact ?? user.PhoneNumber,
|
||||||
|
ServiceAreas = areas,
|
||||||
|
PendingRegistration = user.PendingRegistration == true,
|
||||||
|
Status = user.UniqueName ?? "Active",
|
||||||
|
IsAccountOwner = user.IsAccountOwner,
|
||||||
|
Permissions = profile?.Value?.Permissions ?? Array.Empty<TeamPermissionValueDTO>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string? Validate(
|
private static string? Validate(
|
||||||
CreateTeamMemberRequestDTO request,
|
CreateTeamMemberRequestDTO request,
|
||||||
out string? role,
|
out string? role,
|
||||||
|
|
@ -211,4 +336,7 @@ public sealed class TeamMemberService : ITeamMemberService
|
||||||
|
|
||||||
private static CreateTeamMemberOutcomeDTO Failure(string error) =>
|
private static CreateTeamMemberOutcomeDTO Failure(string error) =>
|
||||||
new() { Success = false, Error = error };
|
new() { Success = false, Error = error };
|
||||||
|
|
||||||
|
private static TeamMemberOperationOutcomeDTO OperationFailure(string error) =>
|
||||||
|
new() { Success = false, Error = error };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,13 @@ public interface ITeamMemberService
|
||||||
CreateTeamMemberRequestDTO request,
|
CreateTeamMemberRequestDTO request,
|
||||||
ClaimsPrincipal caller,
|
ClaimsPrincipal caller,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
Task<TeamMemberOperationOutcomeDTO> GetAsync(
|
||||||
|
string userId,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
Task<TeamMemberOperationOutcomeDTO> UpdateAsync(
|
||||||
|
string userId,
|
||||||
|
UpdateTeamMemberRequestDTO request,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue