diff --git a/Api.SeaHavenIndustries.Tests/TeamMemberServiceTests.cs b/Api.SeaHavenIndustries.Tests/TeamMemberServiceTests.cs index d9ce2d7..5f5204b 100644 --- a/Api.SeaHavenIndustries.Tests/TeamMemberServiceTests.cs +++ b/Api.SeaHavenIndustries.Tests/TeamMemberServiceTests.cs @@ -10,6 +10,7 @@ using Moq; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; +using SeaHaven.Services.Interfaces; using Xunit; namespace Api.SeaHavenIndustries.Tests; @@ -25,7 +26,9 @@ public sealed class TeamMemberServiceTests userManager.Object, roleManager.Object, Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of(), + Mock.Of()); var result = await service.CreateAsync( ValidRequest() with { ServiceAreas = Array.Empty() }, @@ -40,7 +43,7 @@ public sealed class TeamMemberServiceTests [Fact] 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( ValidRequest() with { Color = "#000000" }, @@ -65,7 +68,7 @@ public sealed class TeamMemberServiceTests [Fact] 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; userManager .Setup(manager => manager.CreateAsync(It.IsAny())) @@ -96,10 +99,227 @@ public sealed class TeamMemberServiceTests overrides.Verify(data => data.SetOverridesAsync("new-user", It.Is>(value => value["deleteSites"] == UserPermissionState.Allow), It.IsAny()), 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 { "Admin" }); + userData.Setup(data => data.IsAccountOwnerAsync(owner.Id, It.IsAny())).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(), It.IsAny()), 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 { "Dispatcher" }); + userManager.Setup(manager => manager.FindByEmailAsync("new@example.com")).ReturnsAsync((ApplicationUser?)null); + userManager.Setup(manager => manager.SetEmailAsync(user, "new@example.com")) + .Callback((member, email) => member.Email = email) + .ReturnsAsync(IdentityResult.Success); + userManager.Setup(manager => manager.SetUserNameAsync(user, "new@example.com")) + .Callback((member, email) => member.UserName = email) + .ReturnsAsync(IdentityResult.Success); + userManager.Setup(manager => manager.UpdateSecurityStampAsync(user)) + .ReturnsAsync(IdentityResult.Success); + roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true); + areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny())) + .ReturnsAsync(new[] { "Central" }); + permissions.Setup(data => data.GetProfileAsync(user.Id, It.IsAny(), It.IsAny())) + .ReturnsAsync(TeamPermissionResult.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 + { + ["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()), Times.Once); + userManager.Verify(manager => manager.UpdateSecurityStampAsync(user), Times.Once); + areas.Verify(data => data.ReplaceAsync(user.Id, It.Is>(value => value.SequenceEqual(new[] { "West" })), It.IsAny()), Times.Once); + overrides.Verify(data => data.ClearOverridesAsync(user.Id, It.IsAny()), Times.Once); + overrides.Verify(data => data.SetOverridesAsync(user.Id, It.Is>(value => value["createSites"] == UserPermissionState.Deny), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Update_SavesUnchangedNameWithoutDuplicatingRoleLikeLastName() + { + var service = NewService( + out var userManager, + out var roleManager, + out var areas, + out _, + out var userData, + out var permissions); + var user = new ApplicationUser + { + Id = "admin", + FirstName = "Legacy", + LastName = "Manager", + Email = "legacy@example.com", + Color = "#F59E0B", + UniqueName = "Active" + }; + userManager.Setup(manager => manager.FindByIdAsync(user.Id)).ReturnsAsync(user); + userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(new List { "Admin" }); + userManager.Setup(manager => manager.FindByEmailAsync(user.Email!)).ReturnsAsync(user); + roleManager.Setup(manager => manager.RoleExistsAsync("Admin")).ReturnsAsync(true); + areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny())) + .ReturnsAsync(Array.Empty()); + permissions.Setup(data => data.GetProfileAsync(user.Id, It.IsAny(), It.IsAny())) + .ReturnsAsync(TeamPermissionResult.Success(new TeamPermissionProfileDTO + { + UserId = user.Id, + Permissions = Array.Empty() + })); + + var result = await service.UpdateAsync( + user.Id, + new UpdateTeamMemberRequestDTO + { + Name = "Legacy Manager", + Role = "Admin", + Color = "#F59E0B", + Email = user.Email + }, + Admin(), + CancellationToken.None); + + result.Success.Should().BeTrue(); + user.FirstName.Should().Be("Legacy"); + user.LastName.Should().Be("Manager"); + result.Member!.Name.Should().Be("Legacy Manager"); + userData.Verify(data => data.UpdateUserAsync(user, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Update_LeavesOmittedOverridesUnchanged() + { + 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 = "taylor@example.com", + Color = "#F59E0B", + UniqueName = "Active" + }; + userManager.Setup(manager => manager.FindByIdAsync(user.Id)).ReturnsAsync(user); + userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(new List { "Dispatcher" }); + roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true); + areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny())).ReturnsAsync(Array.Empty()); + permissions.Setup(data => data.GetProfileAsync(user.Id, It.IsAny(), It.IsAny())) + .ReturnsAsync(TeamPermissionResult.Success(new TeamPermissionProfileDTO + { + UserId = user.Id, + Permissions = Array.Empty() + })); + + var result = await service.UpdateAsync( + user.Id, + new UpdateTeamMemberRequestDTO + { + Name = "Taylor", + Role = "Dispatcher", + Color = "#F59E0B", + Email = user.Email, + ServiceAreas = new[] { "East" } + }, + Admin(), + CancellationToken.None); + + result.Success.Should().BeTrue(); + overrides.Verify(data => data.ClearOverridesAsync(It.IsAny(), It.IsAny()), Times.Never); + overrides.Verify(data => data.SetOverridesAsync(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Never); + userData.Verify(data => data.UpdateUserAsync(user, It.IsAny()), Times.Once); + } + [Fact] public async Task Create_ConcurrentDuplicateEmail_ReturnsAlreadyInUseInsteadOf500() { - var service = NewService(out var userManager, out var roleManager, out _, out _); + var service = NewService(out var userManager, out var roleManager, out _, out _, out _, out _); userManager .Setup(manager => manager.FindByEmailAsync(It.IsAny())) .ReturnsAsync((ApplicationUser?)null); @@ -120,13 +340,23 @@ public sealed class TeamMemberServiceTests out Mock> userManager, out Mock> roleManager, out Mock areas, - out Mock overrides) + out Mock overrides, + out Mock userData, + out Mock permissions) { userManager = UserManager(); roleManager = RoleManager(); areas = new Mock(); overrides = new Mock(); - return new TeamMemberService(userManager.Object, roleManager.Object, areas.Object, overrides.Object); + userData = new Mock(); + permissions = new Mock(); + return new TeamMemberService( + userManager.Object, + roleManager.Object, + areas.Object, + overrides.Object, + userData.Object, + permissions.Object); } private static Mock> UserManager() diff --git a/Api.SeaHavenIndustries/Controllers/TeamMemberController.cs b/Api.SeaHavenIndustries/Controllers/TeamMemberController.cs index d09c6bd..f6537b6 100644 --- a/Api.SeaHavenIndustries/Controllers/TeamMemberController.cs +++ b/Api.SeaHavenIndustries/Controllers/TeamMemberController.cs @@ -33,4 +33,35 @@ public sealed class TeamMemberController : ControllerBase return Ok(outcome.Member); } + + [HttpGet("{userId}")] + public async Task Get(string userId, CancellationToken cancellationToken) + { + var outcome = await _teamMemberService.GetAsync(userId, User, cancellationToken); + return ToActionResult(outcome); + } + + [HttpPut("{userId}")] + public async Task 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 }); + } } diff --git a/SeaHaven.DataServices/Implementation/TeamPermissionOverrideDataService.cs b/SeaHaven.DataServices/Implementation/TeamPermissionOverrideDataService.cs index 4f1d4ab..db90df2 100644 --- a/SeaHaven.DataServices/Implementation/TeamPermissionOverrideDataService.cs +++ b/SeaHaven.DataServices/Implementation/TeamPermissionOverrideDataService.cs @@ -51,19 +51,13 @@ public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideD }; } - public async Task SetOverrideAsync( + public Task SetOverrideAsync( string userId, string permissionKey, UserPermissionState state, CancellationToken cancellationToken) { - await SetOverridesAsync( - userId, - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - [permissionKey] = state - }, - cancellationToken); + return SetOverrideCoreAsync(userId, permissionKey, state, cancellationToken); } public async Task SetOverridesAsync( @@ -108,10 +102,9 @@ public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideD } catch (DbUpdateException) { - // A concurrent write inserted the same composite key between our - // existence check and this save. Converge on the caller's intent. + // A concurrent write (create or PUT) inserted the same composite key + // between our existence check and this save. Converge on the caller's intent. _context.Entry(addition).State = EntityState.Detached; - var winner = await _context.UserPermissionOverrides .SingleAsync( permission => permission.UserId == userId @@ -121,4 +114,11 @@ public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideD await _context.SaveChangesAsync(cancellationToken); } } + + public async Task ClearOverridesAsync(string userId, CancellationToken cancellationToken) + { + await _context.UserPermissionOverrides + .Where(item => item.UserId == userId) + .ExecuteDeleteAsync(cancellationToken); + } } diff --git a/SeaHaven.DataServices/Interfaces/ITeamPermissionOverrideDataService.cs b/SeaHaven.DataServices/Interfaces/ITeamPermissionOverrideDataService.cs index 416c6c9..79d9b9b 100644 --- a/SeaHaven.DataServices/Interfaces/ITeamPermissionOverrideDataService.cs +++ b/SeaHaven.DataServices/Interfaces/ITeamPermissionOverrideDataService.cs @@ -16,4 +16,6 @@ public interface ITeamPermissionOverrideDataService string userId, IReadOnlyDictionary overrides, CancellationToken cancellationToken); + + Task ClearOverridesAsync(string userId, CancellationToken cancellationToken); } diff --git a/SeaHaven.Services.Tests/TeamPermissionServiceTests.cs b/SeaHaven.Services.Tests/TeamPermissionServiceTests.cs index b22042f..33bda2f 100644 --- a/SeaHaven.Services.Tests/TeamPermissionServiceTests.cs +++ b/SeaHaven.Services.Tests/TeamPermissionServiceTests.cs @@ -140,5 +140,11 @@ public sealed class TeamPermissionServiceTests return Task.CompletedTask; } + + public Task ClearOverridesAsync(string userId, CancellationToken cancellationToken) + { + _overrides.Clear(); + return Task.CompletedTask; + } } } diff --git a/SeaHaven.Services/DTOs/TeamMemberDTOs.cs b/SeaHaven.Services/DTOs/TeamMemberDTOs.cs index 8c430b9..814e279 100644 --- a/SeaHaven.Services/DTOs/TeamMemberDTOs.cs +++ b/SeaHaven.Services/DTOs/TeamMemberDTOs.cs @@ -13,7 +13,7 @@ public sealed record CreateTeamMemberRequestDTO public IReadOnlyDictionary? PermissionOverrides { get; init; } } -public sealed class TeamMemberCreatedDTO +public class TeamMemberCreatedDTO { public required string Id { get; init; } public required string Name { get; init; } @@ -25,6 +25,32 @@ public sealed class TeamMemberCreatedDTO public bool PendingRegistration { get; init; } } +public sealed class TeamMemberDetailDTO : TeamMemberCreatedDTO +{ + public string Status { get; init; } = "Active"; + public bool IsAccountOwner { get; init; } + public IReadOnlyList Permissions { get; init; } = Array.Empty(); +} + +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? ServiceAreas { get; init; } + public IReadOnlyDictionary? 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 bool Success { get; init; } diff --git a/SeaHaven.Services/Implementation/TeamMemberService.cs b/SeaHaven.Services/Implementation/TeamMemberService.cs index 7382c3c..ba2cd2b 100644 --- a/SeaHaven.Services/Implementation/TeamMemberService.cs +++ b/SeaHaven.Services/Implementation/TeamMemberService.cs @@ -18,17 +18,23 @@ public sealed class TeamMemberService : ITeamMemberService private readonly RoleManager _roleManager; private readonly IUserServiceAreaDataService _areaDataService; private readonly ITeamPermissionOverrideDataService _permissionDataService; + private readonly IUserDataService _userDataService; + private readonly ITeamPermissionService _permissionService; public TeamMemberService( UserManager userManager, RoleManager roleManager, IUserServiceAreaDataService areaDataService, - ITeamPermissionOverrideDataService permissionDataService) + ITeamPermissionOverrideDataService permissionDataService, + IUserDataService userDataService, + ITeamPermissionService permissionService) { _userManager = userManager; _roleManager = roleManager; _areaDataService = areaDataService; _permissionDataService = permissionDataService; + _userDataService = userDataService; + _permissionService = permissionService; } public async Task CreateAsync( @@ -46,12 +52,9 @@ public sealed class TeamMemberService : ITeamMemberService if (await _userManager.FindByEmailAsync(email!) is not null) return Failure("Email is already in use."); - if (!await _roleManager.RoleExistsAsync(role!)) - { - var roleResult = await _roleManager.CreateAsync(new IdentityRole(role!)); - if (!roleResult.Succeeded) - return Failure(roleResult.Errors.FirstOrDefault()?.Description ?? "Unable to create role."); - } + var roleError = await EnsureRoleAsync(role!); + if (roleError is not null) + return Failure(roleError); var now = DateTime.UtcNow; var user = new ApplicationUser @@ -121,6 +124,168 @@ public sealed class TeamMemberService : ITeamMemberService }; } + public async Task 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 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); + + ApplyName(user, candidate.Name!.Trim()); + user.Contact = candidate.Phone?.Trim(); + user.PhoneNumber = candidate.Phone?.Trim(); + user.Color = color; + user.UniqueName = request.IsActive ? ActiveStatus : "Inactive"; + user.IsDeleted = !request.IsActive; + + if (!string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase)) + { + var emailResult = await _userManager.SetEmailAsync(user, email!); + if (!emailResult.Succeeded) + return OperationFailure(emailResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email."); + + var userNameResult = await _userManager.SetUserNameAsync(user, email!); + if (!userNameResult.Succeeded) + return OperationFailure(userNameResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email."); + + var securityStampResult = await _userManager.UpdateSecurityStampAsync(user); + if (!securityStampResult.Succeeded) + return OperationFailure(securityStampResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email."); + } + + if (roleChanged) + { + if (existingRoles.Count > 0) + { + var removeRoleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles); + if (!removeRoleResult.Succeeded) + return OperationFailure(removeRoleResult.Errors.FirstOrDefault()?.Description ?? "Unable to update role."); + } + + var addRoleResult = await _userManager.AddToRoleAsync(user, role!); + if (!addRoleResult.Succeeded) + return OperationFailure(addRoleResult.Errors.FirstOrDefault()?.Description ?? "Unable to update role."); + } + + await _userDataService.UpdateUserAsync(user, cancellationToken); + await _areaDataService.ReplaceAsync(user.Id, areas!, cancellationToken); + if (roleChanged || request.PermissionOverrides is not null) + { + await _permissionDataService.ClearOverridesAsync(user.Id, cancellationToken); + if (request.PermissionOverrides is not null) + await _permissionDataService.SetOverridesAsync(user.Id, overrides!, cancellationToken); + } + + return await OperationSuccessAsync(user, caller, cancellationToken); + } + + private static void ApplyName(ApplicationUser user, string fullName) + { + var current = $"{user.FirstName ?? ""} {user.LastName ?? ""}".Trim(); + if (string.Equals(fullName, current, StringComparison.Ordinal)) + return; + + var separator = fullName.IndexOf(' '); + if (separator < 0) + { + user.FirstName = fullName; + user.LastName = null; + return; + } + + user.FirstName = fullName[..separator].Trim(); + user.LastName = fullName[(separator + 1)..].Trim(); + } + + private async Task 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 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() + } + }; + } + private static string? Validate( CreateTeamMemberRequestDTO request, out string? role, @@ -225,4 +390,7 @@ public sealed class TeamMemberService : ITeamMemberService private static CreateTeamMemberOutcomeDTO Failure(string error) => new() { Success = false, Error = error }; + + private static TeamMemberOperationOutcomeDTO OperationFailure(string error) => + new() { Success = false, Error = error }; } diff --git a/SeaHaven.Services/Interfaces/ITeamMemberService.cs b/SeaHaven.Services/Interfaces/ITeamMemberService.cs index 36016cf..702625f 100644 --- a/SeaHaven.Services/Interfaces/ITeamMemberService.cs +++ b/SeaHaven.Services/Interfaces/ITeamMemberService.cs @@ -9,4 +9,13 @@ public interface ITeamMemberService CreateTeamMemberRequestDTO request, ClaimsPrincipal caller, CancellationToken cancellationToken); + Task GetAsync( + string userId, + ClaimsPrincipal caller, + CancellationToken cancellationToken); + Task UpdateAsync( + string userId, + UpdateTeamMemberRequestDTO request, + ClaimsPrincipal caller, + CancellationToken cancellationToken); }