refactor(identity): enforce service and data boundaries

This commit is contained in:
Alexandre Brandizzi 2026-07-24 11:03:43 -03:00
parent 445cd68468
commit c440b2e3ee
20 changed files with 1637 additions and 317 deletions

View file

@ -0,0 +1,231 @@
using Api.SeaHavenIndustries.Controllers;
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
using System.Text.Json;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class AuthenticationControllerTests
{
private static string Json(object? value) =>
JsonSerializer.Serialize(value, value?.GetType() ?? typeof(object));
private static AuthenticationController NewController(Mock<IAuthenticationService> service, string? userId = null)
{
var controller = new AuthenticationController(service.Object);
if (userId != null)
{
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId)
}, "Test"))
}
};
}
return controller;
}
[Fact]
public async Task Login_WithSuccessfulResult_MapsLowercaseTokenPayload()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.LoginAsync("alice", "pw", It.IsAny<CancellationToken>()))
.ReturnsAsync(new LoginResultDTO
{
Token = "tok",
Expiration = new DateTime(2026, 1, 2, 3, 4, 5, DateTimeKind.Utc),
Email = "alice@example.com",
UserRole = "Admin",
PhoneNumber = "555",
Fullname = "Alice Q",
Id = "user-1"
});
var controller = NewController(service);
var result = await controller.Login(new LoginModel { Username = "alice", Password = "pw" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var json = Json(ok.Value);
json.Should().Contain("\"token\":\"tok\"");
json.Should().Contain("\"expiration\":\"");
json.Should().Contain("\"email\":\"alice@example.com\"");
json.Should().Contain("\"userRoles\":\"Admin\"");
json.Should().Contain("\"phoneNumber\":\"555\"");
json.Should().Contain("\"fullname\":\"Alice Q\"");
json.Should().Contain("\"id\":\"user-1\"");
}
[Fact]
public async Task Login_WithNullResult_ReturnsUnauthorized()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.LoginAsync(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((LoginResultDTO?)null);
var controller = NewController(service);
var result = await controller.Login(new LoginModel(), CancellationToken.None);
result.Should().BeOfType<UnauthorizedResult>();
}
[Fact]
public async Task ChangePassword_Success_ReturnsSuccessMessageWithTrailingSpace()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.ChangePasswordAsync("42", "old", "new", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
var controller = NewController(service, "42");
var result = await controller.ChangePassword(new ChangePasswords { Currentpassword = "old", Confirmpassword = "new" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<Response>().Subject;
response.Status.Should().Be("Success ");
response.Message.Should().Be("Password successfully changed");
}
[Fact]
public async Task ChangePassword_Failure_ReturnsOldPasswordIncorrectStatus()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.ChangePasswordAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var controller = NewController(service, "42");
var result = await controller.ChangePassword(new ChangePasswords(), CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Status.Should().Be("Old Password is incorrect");
}
[Fact]
public async Task UpdateProfile_MapsProfileDataIntoDataResponse()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.UpdateProfileAsync("42", It.IsAny<UpdateProfileRequestDTO>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new UserProfileDTO { FirstName = "Bob", Email = "bob@x.com", Contact = "111" });
var controller = NewController(service, "42");
var form = new User_DTO { Name = "Bob", Email = "bob@x.com", Contact = "111" };
var result = await controller.UserProfileUpdate(form, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().Subject;
response.Status.Should().Be("200");
response.Message.Should().Be("Introduction Updated Successfully");
var json = Json((object?)response.Data);
json.Should().Contain("\"FirstName\":\"Bob\"");
json.Should().Contain("\"Email\":\"bob@x.com\"");
json.Should().Contain("\"Contact\":\"111\"");
}
[Fact]
public async Task ForgetPassword_Found_ReturnsCheckEmailMessage()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.ForgetPasswordAsync("a@b.com", It.IsAny<CancellationToken>())).ReturnsAsync(true);
var controller = NewController(service);
var result = await controller.ForgetPassword("a@b.com", CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<Response>().Subject;
response.Status.Should().Be("Success ");
response.Message.Should().Be("Please check your email for code");
}
[Fact]
public async Task ForgetPassword_NotFound_ReturnsNoSuchEmailMessage()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.ForgetPasswordAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(false);
var controller = NewController(service);
var result = await controller.ForgetPassword("x@y.com", CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Status.Should().Be("Error");
response.Message.Should().Be("No such email is registered");
}
[Theory]
[InlineData(true, "Success ", "Code Matched")]
[InlineData(false, "Error", "Code Not Matched")]
public async Task VerificationCode_MapsServiceResult(bool matched, string expectedStatus, string expectedMessage)
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.VerifyCodeAsync("123456", It.IsAny<CancellationToken>())).ReturnsAsync(matched);
var controller = NewController(service);
var result = await controller.VerificationCode("123456", CancellationToken.None);
if (matched)
{
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var r = ok.Value.Should().BeOfType<Response>().Subject;
r.Status.Should().Be(expectedStatus);
r.Message.Should().Be(expectedMessage);
}
else
{
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var r = bad.Value.Should().BeOfType<Response>().Subject;
r.Status.Should().Be(expectedStatus);
r.Message.Should().Be(expectedMessage);
}
}
[Fact]
public async Task ResetPassword_Matched_ReturnsPasswordChangedMessage()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.ResetPasswordAsync("a@b.com", "123", "new", It.IsAny<CancellationToken>())).ReturnsAsync(true);
var controller = NewController(service);
var result = await controller.ResetPassword(new ForgetPassword_Dto { Email = "a@b.com", Code = "123", Password = "new" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<Response>().Subject;
response.Status.Should().Be("Success ");
response.Message.Should().Be("password changed");
}
[Fact]
public async Task ResetPassword_NotMatched_ReturnsEmailOrCodeNotFoundMessage()
{
var service = new Mock<IAuthenticationService>();
service.Setup(s => s.ResetPasswordAsync(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var controller = NewController(service);
var result = await controller.ResetPassword(new ForgetPassword_Dto { Email = "a@b.com" }, CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Message.Should().Be("Your email or code not found please check");
}
}

View file

@ -0,0 +1,172 @@
using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Moq;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Implementation;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class AuthenticationServiceTests
{
private static AuthenticationService NewService(
Mock<IUserDataService> userData,
Mock<IForgetPasswordDataService> forget,
Mock<IEmailSender> email,
out Mock<IUserRoleStore<ApplicationUser>> store,
out Mock<IPasswordHasher<ApplicationUser>> hasher)
{
var (manager, s, h) = IdentityTestHelpers.CreateUserManager();
store = s;
hasher = h;
return new AuthenticationService(manager, Config(), userData.Object, forget.Object, email.Object);
}
private static IConfiguration Config() => new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["JWT:Secret"] = new string('x', 64),
["JWT:ValidIssuer"] = "issuer",
["JWT:ValidAudience"] = "audience"
})
.Build();
[Fact]
public async Task Login_UnknownUser_ReturnsNull()
{
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IEmailSender>(), out var store, out _);
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
var result = await service.LoginAsync("nobody", "pw", CancellationToken.None);
result.Should().BeNull();
}
[Fact]
public async Task Login_DeletedUser_RejectedBeforePasswordCheck()
{
var deletedUser = IdentityTestHelpers.User(isDeleted: true);
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IEmailSender>(), out var store, out var hasher);
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(deletedUser);
store.Setup(s => s.GetRolesAsync(deletedUser, It.IsAny<CancellationToken>())).ReturnsAsync(new List<string>());
var result = await service.LoginAsync("alice", "pw", CancellationToken.None);
result.Should().BeNull();
hasher.Verify(h => h.VerifyHashedPassword(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Fact]
public async Task Login_ValidUser_ReturnsTokenFirstRoleAndIdentity()
{
var user = IdentityTestHelpers.User();
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IEmailSender>(), out var store, out var hasher);
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(user);
store.Setup(s => s.GetRolesAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync(new List<string> { "Admin", "Manager" });
store.As<Microsoft.AspNetCore.Identity.IUserPasswordStore<ApplicationUser>>()
.Setup(s => s.GetPasswordHashAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync("hash");
hasher.Setup(h => h.VerifyHashedPassword(user, "hash", "pw")).Returns(Microsoft.AspNetCore.Identity.PasswordVerificationResult.Success);
var result = await service.LoginAsync("alice", "pw", CancellationToken.None);
result.Should().NotBeNull();
result!.Id.Should().Be(user.Id);
result.Email.Should().Be(user.Email);
result.PhoneNumber.Should().Be(user.PhoneNumber);
result.Fullname.Should().Be("Alice Q");
result.UserRole.Should().Be("Admin");
result.Token.Should().NotBeNullOrEmpty();
result.Expiration.Should().BeAfter(DateTime.Now.AddDays(9));
}
[Fact]
public async Task Login_BadPassword_ReturnsNull()
{
var user = IdentityTestHelpers.User();
var service = NewService(new Mock<IUserDataService>(), new Mock<IForgetPasswordDataService>(), new Mock<IEmailSender>(), out var store, out var hasher);
store.Setup(s => s.FindByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(user);
store.As<Microsoft.AspNetCore.Identity.IUserPasswordStore<ApplicationUser>>()
.Setup(s => s.GetPasswordHashAsync(user, It.IsAny<CancellationToken>())).ReturnsAsync("hash");
hasher.Setup(h => h.VerifyHashedPassword(user, "hash", "wrong")).Returns(Microsoft.AspNetCore.Identity.PasswordVerificationResult.Failed);
var result = await service.LoginAsync("alice", "wrong", CancellationToken.None);
result.Should().BeNull();
}
[Fact]
public async Task ForgetPassword_RegisteredEmail_ReplacesCodeAndSendsEmail()
{
var user = IdentityTestHelpers.User();
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetByEmailNormalizedAsync("alice@example.com", It.IsAny<CancellationToken>())).ReturnsAsync(user);
var forget = new Mock<IForgetPasswordDataService>();
var email = new Mock<IEmailSender>();
var service = NewService(userData, forget, email, out _, out _);
var found = await service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
found.Should().BeTrue();
forget.Verify(
f => f.ReplaceCodeAsync(
user.Email!,
user.Id,
It.Is<string>(code => code.Length == 6 && code.All(char.IsDigit)),
It.IsAny<CancellationToken>()),
Times.Once);
email.Verify(e => e.SendEmailAsync(user.Email!, "Forget Password Request.", It.Is<string>(b => b.Contains("Your Password Reset Code is:"))), Times.Once);
}
[Fact]
public async Task ForgetPassword_UnknownEmail_DoesNotEmailOrStoreCode()
{
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetByEmailNormalizedAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
var forget = new Mock<IForgetPasswordDataService>();
var email = new Mock<IEmailSender>();
var service = NewService(userData, forget, email, out _, out _);
var found = await service.ForgetPasswordAsync("nope@example.com", CancellationToken.None);
found.Should().BeFalse();
forget.Verify(f => f.ReplaceCodeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
email.Verify(e => e.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task VerifyCode_ForwardsDataServiceResult(bool exists)
{
var forget = new Mock<IForgetPasswordDataService>();
forget.Setup(f => f.CodeExistsAsync("abc", It.IsAny<CancellationToken>())).ReturnsAsync(exists);
var service = NewService(new Mock<IUserDataService>(), forget, new Mock<IEmailSender>(), out _, out _);
var result = await service.VerifyCodeAsync("abc", CancellationToken.None);
result.Should().Be(exists);
}
[Fact]
public async Task ResetPassword_NoMatchingCode_ReturnsFalseWithoutReset()
{
var forget = new Mock<IForgetPasswordDataService>();
forget.Setup(f => f.ExistsByEmailAndCodeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(false);
var service = NewService(new Mock<IUserDataService>(), forget, new Mock<IEmailSender>(), out var store, out _);
var result = await service.ResetPasswordAsync("a@b.com", "999", "new", CancellationToken.None);
result.Should().BeFalse();
store.Verify(s => s.FindByIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
forget.Verify(f => f.RemoveByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
}

View file

@ -0,0 +1,42 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
namespace Api.SeaHavenIndustries.Tests;
internal static class IdentityTestHelpers
{
public static (UserManager<ApplicationUser> manager, Mock<IUserRoleStore<ApplicationUser>> store, Mock<IPasswordHasher<ApplicationUser>> hasher) CreateUserManager()
{
var store = new Mock<IUserRoleStore<ApplicationUser>>();
var hasher = new Mock<IPasswordHasher<ApplicationUser>>();
store.As<IUserPasswordStore<ApplicationUser>>();
var manager = new UserManager<ApplicationUser>(
store.Object,
Options.Create(new IdentityOptions()),
hasher.Object,
new List<IUserValidator<ApplicationUser>>(),
new List<IPasswordValidator<ApplicationUser>>(),
new Mock<ILookupNormalizer>().Object,
new IdentityErrorDescriber(),
new Mock<IServiceProvider>().Object,
new Mock<ILogger<UserManager<ApplicationUser>>>().Object);
return (manager, store, hasher);
}
public static ApplicationUser User(string id = "u1", string? userName = "alice", bool? isDeleted = false) => new()
{
Id = id,
UserName = userName,
Email = userName + "@example.com",
FirstName = "Alice",
LastName = "Q",
PhoneNumber = "555",
IsDeleted = isDeleted
};
}

View file

@ -0,0 +1,190 @@
using Api.SeaHavenIndustries.Controllers;
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
using System.Text.Json;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class UserControllerTests
{
private static string Json(object? value) =>
JsonSerializer.Serialize(value, value?.GetType() ?? typeof(object));
private static UserController NewController(Mock<IUserService> service, string? userId = null)
{
var controller = new UserController(service.Object);
if (userId != null)
{
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId)
}, "Test"))
}
};
}
return controller;
}
[Fact]
public async Task GetUsers_ReturnsServiceList()
{
var service = new Mock<IUserService>();
service.Setup(s => s.GetUsersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UserListRowDTO>
{
new() { Id = "1", Name = "A", Email = "a@x.com", RoleName = "Admin", Status = "Active", Date = "Jan 01 2026" }
});
var controller = NewController(service);
var result = await controller.GetUsers(CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
ok.Value.Should().BeAssignableTo<IEnumerable<UserListRowDTO>>();
}
[Fact]
public async Task AddUser_Success_ReturnsUpdatedSuccessfully()
{
var service = new Mock<IUserService>();
service.Setup(s => s.AddUserAsync(It.IsAny<AddUserRequestDTO>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = true });
var controller = NewController(service);
var result = await controller.AddUser(new User_DTO { Name = "N", Email = "n@x.com", Role = "Admin" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().Subject;
response.Status.Should().Be("200");
response.Message.Should().Be("Updated Successfully");
}
[Fact]
public async Task AddUser_Failure_ReturnsBadRequestWithServiceError()
{
var service = new Mock<IUserService>();
service.Setup(s => s.AddUserAsync(It.IsAny<AddUserRequestDTO>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = "boom" });
var controller = NewController(service);
var result = await controller.AddUser(new User_DTO(), CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Status.Should().Be("Error");
response.Message.Should().Be("boom");
}
[Fact]
public async Task EditUser_NotFound_ReturnsUserNotFoundMessage()
{
var service = new Mock<IUserService>();
service.Setup(s => s.EditUserAsync(It.IsAny<EditUserRequestDTO>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var controller = NewController(service);
var result = await controller.EditUser(new EditUser_DTO { Id = "9" }, CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Message.Should().Be("User not found");
}
[Fact]
public async Task EditUser_Found_ReturnsUpdatedSuccessfully()
{
var service = new Mock<IUserService>();
service.Setup(s => s.EditUserAsync(It.IsAny<EditUserRequestDTO>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
var controller = NewController(service);
var result = await controller.EditUser(new EditUser_DTO { Id = "9" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
ok.Value.Should().BeOfType<DataResponse>();
}
[Fact]
public async Task DeleteUser_NotFound_ReturnsIdNotMatchedMessage()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var controller = NewController(service);
var result = await controller.DeleteUser(new EditUser_DTO { Id = "missing" }, CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Message.Should().Be("ID not matched!");
}
[Fact]
public async Task DeleteUser_Found_DelegatesAndReturnsOk()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync("5", It.IsAny<CancellationToken>())).ReturnsAsync(true);
var controller = NewController(service);
var result = await controller.DeleteUser(new EditUser_DTO { Id = "5" }, CancellationToken.None);
result.Should().BeOfType<OkObjectResult>();
service.Verify(s => s.DeleteUserAsync("5", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task DeleteCurrentUser_UsesClaimUserId()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteCurrentUserAsync("77", It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
var controller = NewController(service, "77");
var result = await controller.DeleteCurrentUser(CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
ok.Value.Should().BeOfType<DataResponse>();
service.Verify(s => s.DeleteCurrentUserAsync("77", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task UserProfile_PreservesLowercaseAddedDateKey()
{
var service = new Mock<IUserService>();
service.Setup(s => s.GetUserProfileAsync("77", It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UserProfileRowDTO>
{
new() { Id = "77", Name = "Me", AddedDate = new DateTime(2026, 1, 1), Email = "me@x.com", Contact = "321" }
});
var controller = NewController(service, "77");
var result = await controller.UserProfile(CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var json = Json(ok.Value);
json.Should().Contain("\"Id\":\"77\"");
json.Should().Contain("\"Name\":\"Me\"");
json.Should().Contain("\"addeddate\":\"");
json.Should().Contain("\"Email\":\"me@x.com\"");
json.Should().Contain("\"Contact\":\"321\"");
}
}

View file

@ -0,0 +1,195 @@
using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Moq;
using SeaHaven.DataServices.Dto;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Implementation;
using SeaHaven.Services.Interfaces;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class UserServiceTests
{
private static UserService NewService(
Mock<IUserDataService> userData,
Mock<IEmailSender> email,
out Mock<IUserRoleStore<ApplicationUser>> store)
{
var (manager, s, _) = IdentityTestHelpers.CreateUserManager();
store = s;
return new UserService(manager, userData.Object, email.Object);
}
[Fact]
public async Task GetUsers_FormatsCreatedDate_AndHandlesNullDate()
{
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetAllForListAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UserListRowData>
{
new() { Id = "1", Name = "A", Email = "a@x.com", RoleName = "Admin", Status = "Active", CreatedDate = new DateTime(2026, 1, 5) },
new() { Id = "2", Name = "B", Email = "b@x.com", RoleName = "Tech", Status = "Active", CreatedDate = null }
});
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var users = (await service.GetUsersAsync(CancellationToken.None)).ToList();
users.Should().HaveCount(2);
users[0].Date.Should().Be("Jan 05 2026");
users[0].RoleName.Should().Be("Admin");
users[0].Status.Should().Be("Active");
users[1].Date.Should().Be("");
users[1].Name.Should().Be("B");
}
[Fact]
public async Task DeleteUser_NotFound_ReturnsFalseWithoutCascade()
{
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetForEditAsync("missing", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var found = await service.DeleteUserAsync("missing", CancellationToken.None);
found.Should().BeFalse();
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task DeleteUser_Found_PerformsCascade()
{
var user = IdentityTestHelpers.User();
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var found = await service.DeleteUserAsync("u1", CancellationToken.None);
found.Should().BeTrue();
userData.Verify(u => u.DeleteUserWithCascadeAsync(user, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task DeleteCurrentUser_SetsIsDeletedFlag()
{
var user = IdentityTestHelpers.User();
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
var service = NewService(userData, new Mock<IEmailSender>(), out _);
await service.DeleteCurrentUserAsync("u1", CancellationToken.None);
user.IsDeleted.Should().BeTrue();
userData.Verify(u => u.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task EditUser_NotFound_ReturnsFalse()
{
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetForEditAsync("x", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var succeeded = await service.EditUserAsync(new EditUserRequestDTO { Id = "x", Email = "e@x.com", Name = "n", Role = "Admin" }, CancellationToken.None);
succeeded.Should().BeFalse();
}
[Fact]
public async Task GetUserProfile_MapsCreatedDateToAddedDate()
{
var user = new UserProfileData
{
Id = "u1",
FirstName = "Alice",
CreatedDate = new DateTime(2026, 3, 4)
};
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetProfileAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var rows = await service.GetUserProfileAsync("u1", CancellationToken.None);
rows.Should().HaveCount(1);
rows[0].AddedDate.Should().Be(new DateTime(2026, 3, 4));
rows[0].Name.Should().Be("Alice");
}
[Fact]
public async Task AddUser_CreateSuccess_PersistsRoleAndEmailsLoginInformation()
{
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetByIdAsync(It.IsAny<string>())).ReturnsAsync((ApplicationUser?)null);
var email = new Mock<IEmailSender>();
var service = NewService(userData, email, out var store);
store.As<IUserPasswordStore<ApplicationUser>>()
.Setup(s => s.SetPasswordHashAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
store.Setup(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(IdentityResult.Success);
store.Setup(s => s.AddToRoleAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
var outcome = await service.AddUserAsync(new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "Admin" }, CancellationToken.None);
outcome.Success.Should().BeTrue();
store.Verify(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Once);
store.Verify(s => s.AddToRoleAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
email.Verify(
e => e.SendEmailAsync(
"n@x.com",
"Login Information",
It.Is<string>(body => ContainsStrongGeneratedPassword(body))),
Times.Once);
}
[Fact]
public async Task AddUser_CreateFails_ReturnsFirstError()
{
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetByIdAsync(It.IsAny<string>())).ReturnsAsync((ApplicationUser?)null);
var service = NewService(userData, new Mock<IEmailSender>(), out var store);
store.As<IUserPasswordStore<ApplicationUser>>()
.Setup(s => s.SetPasswordHashAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
store.Setup(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "duplicate" }));
var outcome = await service.AddUserAsync(new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "Admin" }, CancellationToken.None);
outcome.Success.Should().BeFalse();
outcome.Error.Should().Be("duplicate");
}
private static bool ContainsStrongGeneratedPassword(string body)
{
const string marker = "Your password is: ";
var start = body.IndexOf(marker, StringComparison.Ordinal);
if (start < 0)
return false;
var password = body[(start + marker.Length)..]
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault();
return password is
{
Length: 8
}
&& password.Any(char.IsUpper)
&& password.Any(char.IsLower)
&& password.Any(char.IsDigit)
&& password.Any(character => !char.IsLetterOrDigit(character));
}
}

View file

@ -1,14 +1,10 @@
using Data.SeaHavenIndustries;
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
using System.Text;
using Api.SeaHavenIndustries.DTOs;
using Api.SeaHavenIndustries.Helper;
using Microsoft.EntityFrameworkCore;
namespace Api.SeaHavenIndustries.Controllers
{
@ -16,55 +12,35 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("api/Authentication")]
public class AuthenticationController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IConfiguration _configuration;
private readonly ApplicationDbContext _db;
private readonly SendMessage _sendMessage;
private readonly IAuthenticationService _authenticationService;
public AuthenticationController(UserManager<ApplicationUser> userManager, IConfiguration configuration, ApplicationDbContext db, SendMessage sendMessage)
public AuthenticationController(IAuthenticationService authenticationService)
{
_userManager = userManager;
_configuration = configuration;
_db = db;
_sendMessage = sendMessage;
_authenticationService = authenticationService;
}
[AllowAnonymous]
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
public async Task<IActionResult> Login([FromBody] LoginModel model, CancellationToken cancellationToken)
{
try
{
var user = await _userManager.FindByNameAsync(model.Username ?? "");
if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, model.Password ?? ""))
var result = await _authenticationService.LoginAsync(model.Username, model.Password, cancellationToken);
if (result != null)
{
// Standard login without 2FA
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName ?? ""),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var token = GetToken(authClaims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo,
email = user.Email,
userRoles = userRoles.FirstOrDefault(),
phoneNumber = user.PhoneNumber,
fullname = user.FirstName + " " + user.LastName,
id = user.Id
token = result.Token,
expiration = result.Expiration,
email = result.Email,
userRoles = result.UserRole,
phoneNumber = result.PhoneNumber,
fullname = result.Fullname,
id = result.Id
});
}
// Invalid credentials
return Unauthorized();
}
catch (Exception ex)
@ -72,17 +48,16 @@ namespace Api.SeaHavenIndustries.Controllers
return StatusCode(500, ex.Message);
}
}
[Route("ChangePassword")]
[HttpPost]
public async Task<IActionResult> ChangePassword(ChangePasswords usermodel)
public async Task<IActionResult> ChangePassword(ChangePasswords usermodel, CancellationToken cancellationToken)
{
var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
var user = await _userManager.FindByIdAsync(userid);
var result = await _userManager.ChangePasswordAsync(user, usermodel.Currentpassword ?? "", usermodel.Confirmpassword ?? "");
if (result.Succeeded)
var succeeded = await _authenticationService.ChangePasswordAsync(userid, usermodel.Currentpassword, usermodel.Confirmpassword, cancellationToken);
if (succeeded)
{
return Ok(new Response { Status = "Success ", Message = "Password successfully changed" });
}
@ -90,44 +65,31 @@ namespace Api.SeaHavenIndustries.Controllers
return BadRequest(new Response { Status = "Old Password is incorrect" });
}
private JwtSecurityToken GetToken(List<Claim> authClaims)
{
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"] ?? ""));
var token = new JwtSecurityToken(
issuer: _configuration["JWT:ValidIssuer"],
audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddDays(10),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return token;
}
[HttpPost]
[Route("UpdateProfile")]
public async Task<IActionResult> UserProfileUpdate([FromForm] User_DTO model)
public async Task<IActionResult> UserProfileUpdate([FromForm] User_DTO model, CancellationToken cancellationToken)
{
try
{
var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
ApplicationUser appuser = _db.Users.AsNoTracking().Where(u => u.Id == userid).FirstOrDefault() ?? new ApplicationUser();
appuser.FirstName = model.Name;
appuser.Email = model.Email;
appuser.Contact = model.Contact;
_db.Users.Update(appuser);
await _db.SaveChangesAsync();
var dto = new UpdateProfileRequestDTO
{
Name = model.Name,
Email = model.Email,
Contact = model.Contact
};
var data = await _authenticationService.UpdateProfileAsync(userid, dto, cancellationToken);
return Ok(new DataResponse
{
Message = "Introduction Updated Successfully",
Status = "200",
Data = _db.Users.Where(u => u.Id == userid).Select(s => new
Data = data == null ? null : new
{
s.FirstName,
//s.Location,
s.Email,
s.Contact
}).FirstOrDefault()
data.FirstName,
data.Email,
data.Contact
}
});
}
catch (Exception ex)
@ -141,25 +103,11 @@ namespace Api.SeaHavenIndustries.Controllers
[AllowAnonymous]
[HttpPost()]
[Route("ForgetPassword")]
public async Task<IActionResult> ForgetPassword(string Email)
public async Task<IActionResult> ForgetPassword(string Email, CancellationToken cancellationToken)
{
var user = await _db.Users.Where(u => u.Email.ToLower().Trim() == Email.ToLower().ToLower()).FirstOrDefaultAsync();
if (user != null)
var found = await _authenticationService.ForgetPasswordAsync(Email, cancellationToken);
if (found)
{
if (_db.ForgetPasswordCodes.Where(u => u.Email.ToLower().Trim() == Email.ToLower().Trim()).Any())
{
_db.ForgetPasswordCodes.Remove(_db.ForgetPasswordCodes.Where(u => u.Email.ToLower().Trim() == Email.ToLower().Trim()).FirstOrDefault());
_db.SaveChanges();
}
ForgetPasswordCode forgetPasswordCode = new ForgetPasswordCode();
forgetPasswordCode.Email = user.Email ?? "";
forgetPasswordCode.UserId = user.Id;
forgetPasswordCode.Code = GenerateRandomNo();
_db.ForgetPasswordCodes.Add(forgetPasswordCode);
_db.SaveChanges();
string body = $"Your Password Reset Code is: " + forgetPasswordCode.Code;
await _sendMessage.SendEMail(user.Email, "Forget Password Request.", body);
return Ok(new Response { Status = "Success ", Message = "Please check your email for code" });
}
else
@ -171,11 +119,11 @@ namespace Api.SeaHavenIndustries.Controllers
[AllowAnonymous]
[HttpPost()]
[Route("VerificationCode")]
public async Task<IActionResult> VerificationCode(string code)
public async Task<IActionResult> VerificationCode(string code, CancellationToken cancellationToken)
{
try
{
if (await _db.ForgetPasswordCodes.Where(u => u.Code == code).AnyAsync())
if (await _authenticationService.VerifyCodeAsync(code, cancellationToken))
{
return Ok(new Response { Status = "Success ", Message = "Code Matched" });
@ -195,16 +143,12 @@ namespace Api.SeaHavenIndustries.Controllers
[AllowAnonymous]
[HttpPost()]
[Route("ResetPassword")]
public async Task<IActionResult> ResetPassword(ForgetPassword_Dto fpdto)
public async Task<IActionResult> ResetPassword(ForgetPassword_Dto fpdto, CancellationToken cancellationToken)
{
try
{
if (_db.ForgetPasswordCodes.Where(u => u.Email.ToLower().Trim() == fpdto.Email.ToLower().Trim() && u.Code == fpdto.Code.Trim()).Any())
if (await _authenticationService.ResetPasswordAsync(fpdto.Email, fpdto.Code, fpdto.Password, cancellationToken))
{
var GetUser = _db.ForgetPasswordCodes.Where(u => u.Email.ToLower().Trim() == fpdto.Email.ToLower().Trim()).FirstOrDefault();
var user = await _userManager.FindByIdAsync(GetUser.UserId);
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var result = await _userManager.ResetPasswordAsync(user, token, fpdto.Password);
return Ok(new Response { Status = "Success ", Message = "password changed" });
}
else
@ -219,12 +163,6 @@ namespace Api.SeaHavenIndustries.Controllers
}
}
private Random _random = new Random();
private string GenerateRandomNo()
{
return _random.Next(0, 999999).ToString("D6");
}
#endregion
}
}

View file

@ -1,15 +1,10 @@
using Api.SeaHavenIndustries.DTOs;
using Api.SeaHavenIndustries.Helper;
using Api.SeaHavenIndustries.Models;
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PasswordGenerator;
using SendGrid.Helpers.Mail;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
using System.Security.Cryptography;
namespace Api.SeaHavenIndustries.Controllers
{
@ -18,107 +13,36 @@ namespace Api.SeaHavenIndustries.Controllers
[Route("api/User")]
public class UserController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _db;
private readonly SendMessage _sendemail;
private readonly IUserService _userService;
public UserController(UserManager<ApplicationUser> userManager, ApplicationDbContext db, SendMessage sendemail)
public UserController(IUserService userService)
{
_userManager = userManager;
_db = db;
_sendemail = sendemail;
_userService = userService;
}
[Route("")]
[HttpGet]
public async Task<IActionResult> GetUsers()
public async Task<IActionResult> GetUsers(CancellationToken cancellationToken)
{
_db.ChangeTracker.Clear();
var allUsers = _db.Users.Select(s => new UserRoleViewModel
{
RoleName = s.PhoneNumber,
Email = s.Email,
Name = s.FirstName,
Date = s.CreatedDate != null ? s.CreatedDate.Value.Date.ToString("MMM dd yyyy") : "",
Status = s.UniqueName,
Id = s.Id
}).AsQueryable();
return Ok(allUsers);
var users = await _userService.GetUsersAsync(cancellationToken);
return Ok(users);
}
[HttpPost]
[Route("AddUser")]
public async Task<IActionResult> AddUser(User_DTO user)
public async Task<IActionResult> AddUser(User_DTO user, CancellationToken cancellationToken)
{
var model = new ApplicationUser
{
UserName = user.Email,
FirstName = user.Name,
Email = user.Email,
};
try
{
_db.ChangeTracker.Clear();
var exist = _db.Set<ApplicationUser>().Find(model.Id);
if (exist == null)
var dto = new AddUserRequestDTO
{
model.EmailConfirmed = true;
model.UserName = model.Email;
model.CreatedDate = DateTime.Now;
model.UniqueName = "Active";
model.PhoneNumber = user.Role;
var pwd = new Password(8).IncludeLowercase().IncludeUppercase().IncludeSpecial().IncludeNumeric();
var password = pwd.Next();
var result = await _userManager.CreateAsync(model, password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(model, user.Role ?? "");
}
else
{
return BadRequest(new Response { Status = "Error", Message = result.Errors.FirstOrDefault()?.Description ?? "error" });
}
var request = HttpContext?.Request;
//var domain = $"{request?.Scheme}://{request?.Host}";
var domain = "http://shoc-ui-app.s3-website-us-east-1.amazonaws.com/#";
string subject = "Login Information";
string greeting = $"Dear {model.FirstName},\n\n";
string loginLink = $"Click here to login:<a href='{domain}/Login'>Login</a>\n";
string passwordInfo = $"Your password is: {password}\n\n";
string body = $"{greeting}\n\n{loginLink}\n\n{passwordInfo}";
await _sendemail.SendEMail(model?.Email ?? "", subject, body);
}
else
Name = user.Name,
Email = user.Email,
Contact = user.Contact,
Role = user.Role
};
var outcome = await _userService.AddUserAsync(dto, cancellationToken);
if (!outcome.Success)
{
var exist1 = await _userManager.Users.FirstOrDefaultAsync(w => w.Id == model.Id);
if (exist1 == null)
return BadRequest(new Response { Status = "Error", Message = "User not found" });
var existingRole = await _userManager.GetRolesAsync(exist1);
// Remove from roles if any exist
if (existingRole != null && existingRole.Any() && existingRole.FirstOrDefault() != exist1.PhoneNumber)
{
await _userManager.RemoveFromRolesAsync(exist1, existingRole);
}
// Update user details
exist1.FirstName = model.FirstName;
exist1.LastName = model.LastName;
exist1.Contact = model.Contact;
exist1.PhoneNumber = model.PhoneNumber;
// Optionally, detach the entity if you're about to attach it again
_db.Entry(exist1).State = EntityState.Detached;
// Add to role after setting properties
await _userManager.AddToRoleAsync(exist1, user.Role ?? "");
var result = await _userManager.UpdateAsync(exist1);
//await _context.SaveChangesAsync();
return BadRequest(new Response { Status = "Error", Message = outcome.Error });
}
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
@ -131,31 +55,20 @@ namespace Api.SeaHavenIndustries.Controllers
}
[HttpPut]
[Route("EditUser")]
public async Task<IActionResult> EditUser(EditUser_DTO user)
public async Task<IActionResult> EditUser(EditUser_DTO user, CancellationToken cancellationToken)
{
try
{
var exist = _db.Users.Where(w => w.Id == user.Id).FirstOrDefault();
if (exist == null)
return BadRequest(new Response { Status = "Error", Message = "User not found" });
exist.FirstName = user.Name;
exist.EmailConfirmed = true;
exist.UserName = user.Email;
exist.Email = user.Email;
exist.NormalizedEmail = user.Email.ToUpper();
exist.NormalizedUserName = user.Email.ToUpper();
exist.CreatedDate = DateTime.Now;
exist.UniqueName = "Active";
exist.PhoneNumber = user.Role;
var existingRole = await _userManager.GetRolesAsync(exist);
if (existingRole != null && existingRole.Any())
var dto = new EditUserRequestDTO
{
await _userManager.RemoveFromRolesAsync(exist, existingRole);
}
await _userManager.AddToRoleAsync(exist, user.Role ?? "");
await _db.SaveChangesAsync();
Id = user.Id,
Name = user.Name,
Email = user.Email,
Role = user.Role
};
var succeeded = await _userService.EditUserAsync(dto, cancellationToken);
if (!succeeded)
return BadRequest(new Response { Status = "Error", Message = "User not found" });
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (Exception ex)
@ -163,53 +76,19 @@ namespace Api.SeaHavenIndustries.Controllers
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
public class DeleteUserRequest
{
public string Id { get; set; }
}
[HttpDelete]
[Route("DeleteUser")]
public async Task<IActionResult> DeleteUser(EditUser_DTO req)
public async Task<IActionResult> DeleteUser(EditUser_DTO req, CancellationToken cancellationToken)
{
string id = req.Id;
string id = req.Id ?? "";
try
{
var data = await _db.Users.Where(w => w.Id == id).FirstOrDefaultAsync();
if (data != null)
{
var existworkorder = _db.workOrders.Where(w => w.AssignTo.Contains(data.Id)).ToList();
foreach (var workorder in existworkorder)
{
var existdata = _db.workOrders.Where(w => w.Id == workorder.Id).FirstOrDefault();
existdata.AssignTo = null;
}
var existworkordercomments = _db.Comments.Where(w => w.UserId.Contains(data.Id)).ToList();
foreach (var workorderc in existworkordercomments)
{
var existdatacomm = _db.Comments.Where(w => w.Id == workorderc.Id).FirstOrDefault();
existdatacomm.UserId = null;
}
var existtemplate = _db.Templates.Where(w => w.AssignTo.Contains(data.Id)).ToList();
foreach (var workordert in existtemplate)
{
var existdattem = _db.Templates.Where(w => w.Id == workordert.Id).FirstOrDefault();
existdattem.AssignTo = null;
}
var existtempexistuserrolelate = _db.UserRoles.Where(w => w.UserId.Contains(data.Id)).ToList();
if (existtempexistuserrolelate.Count() > 0)
{
_db.UserRoles.RemoveRange(existtempexistuserrolelate);
}
_db.Users.Remove(data);
}
else
var found = await _userService.DeleteUserAsync(id, cancellationToken);
if (!found)
{
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
await _db.SaveChangesAsync();
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (Exception ex)
@ -220,17 +99,12 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpPost]
[Route("DeleteCurrentUser")]
public async Task<IActionResult> DeleteCurrentUser()
public async Task<IActionResult> DeleteCurrentUser(CancellationToken cancellationToken)
{
try
{
var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
var exist = _db.Users.Where(w => w.Id == userid).FirstOrDefault();
if(exist != null)
{
exist.IsDeleted = true;
}
await _db.SaveChangesAsync();
await _userService.DeleteCurrentUserAsync(userid, cancellationToken);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (Exception ex)
@ -241,64 +115,19 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpGet]
[Route("UserProfile")]
public async Task<IActionResult> UserProfile()
public async Task<IActionResult> UserProfile(CancellationToken cancellationToken)
{
var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
var data = await _db.Users.Where(w => w.Id == userid).Select(s => new
var data = await _userService.GetUserProfileAsync(userid, cancellationToken);
return Ok(data.Select(d => new
{
s.Id,
Name = s.FirstName,
addeddate = s.CreatedDate,
s.Email,
s.Contact
}).ToListAsync();
return Ok(data);
}
public static string GenerateRandomPassword(int length = 8)
{
const string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_-+=<>?";
const string capitalLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const string digits = "1234567890";
const string symbols = "!@#$%^&*()_-+=<>?";
using (var rng = RandomNumberGenerator.Create())
{
var passwordChars = new char[length];
// Ensure at least one capital letter
passwordChars[0] = capitalLetters[GetNextNonNegativeIndex(rng, capitalLetters.Length)];
// Ensure at least one digit
passwordChars[1] = digits[GetNextNonNegativeIndex(rng, digits.Length)];
// Ensure at least one symbol
passwordChars[2] = symbols[GetNextNonNegativeIndex(rng, symbols.Length)];
// Fill the rest of the password with random characters
for (int i = 3; i < length; i++)
{
passwordChars[i] = validChars[GetNextNonNegativeIndex(rng, validChars.Length)];
}
// Shuffle the password characters, excluding the initial characters
Random random = new Random();
for (int i = length - 1; i > 2; i--)
{
int j = random.Next(2, i + 1);
(passwordChars[i], passwordChars[j]) = (passwordChars[j], passwordChars[i]);
}
return new string(passwordChars);
}
}
private static int GetNextNonNegativeIndex(RandomNumberGenerator rng, int maxValue)
{
byte[] randomBytes = new byte[4];
rng.GetBytes(randomBytes);
int nextIndex = BitConverter.ToInt32(randomBytes, 0) % maxValue;
return (nextIndex + maxValue) % maxValue; // Ensure the index is non-negative
d.Id,
Name = d.Name,
addeddate = d.AddedDate,
d.Email,
d.Contact
}));
}
}
}

View file

@ -0,0 +1,20 @@
using Api.SeaHavenIndustries.Helper;
using SeaHaven.Services.Interfaces;
namespace Api.SeaHavenIndustries.Helper
{
public class SendGridEmailSender : IEmailSender
{
private readonly SendMessage _sendMessage;
public SendGridEmailSender(SendMessage sendMessage)
{
_sendMessage = sendMessage;
}
public Task<bool> SendEmailAsync(string emailTo, string subject, string body)
{
return _sendMessage.SendEMail(emailTo, subject, body);
}
}
}

View file

@ -38,6 +38,8 @@ builder.Services.AddCors(option =>
option.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
builder.Services.AddScoped<SendMessage>();
builder.Services.AddScoped<Api.SeaHavenIndustries.Helper.SendGridEmailSender>();
builder.Services.AddScoped<SeaHaven.Services.Interfaces.IEmailSender, Api.SeaHavenIndustries.Helper.SendGridEmailSender>();
builder.Services.AddScoped<Api.SeaHavenIndustries.Helper.VendorPortalTokenService>();
builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp =>
{

View file

@ -0,0 +1,21 @@
namespace SeaHaven.DataServices.Dto
{
public sealed class UserListRowData
{
public required string Id { get; init; }
public string? RoleName { get; init; }
public string? Email { get; init; }
public string? Name { get; init; }
public DateTime? CreatedDate { get; init; }
public string? Status { get; init; }
}
public sealed class UserProfileData
{
public required string Id { get; init; }
public string? FirstName { get; init; }
public string? Email { get; init; }
public string? Contact { get; init; }
public DateTime? CreatedDate { get; init; }
}
}

View file

@ -0,0 +1,83 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
{
public class ForgetPasswordDataService : IForgetPasswordDataService
{
private readonly ApplicationDbContext _context;
public ForgetPasswordDataService(ApplicationDbContext context)
{
_context = context;
}
public async Task ReplaceCodeAsync(string email, string userId, string code, CancellationToken cancellationToken)
{
var normalizedEmail = email.ToLower().Trim();
var existing = await _context.ForgetPasswordCodes
.Where(u => u.Email.ToLower().Trim() == normalizedEmail)
.FirstOrDefaultAsync(cancellationToken);
if (existing != null)
{
_context.ForgetPasswordCodes.Remove(existing);
await _context.SaveChangesAsync(cancellationToken);
}
var forgetPasswordCode = new ForgetPasswordCode
{
Email = email,
UserId = userId,
Code = code
};
_context.ForgetPasswordCodes.Add(forgetPasswordCode);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task<bool> CodeExistsAsync(string code, CancellationToken cancellationToken)
{
return await _context.ForgetPasswordCodes
.AsNoTracking()
.Where(u => u.Code == code)
.AnyAsync(cancellationToken);
}
public async Task<bool> ExistsByEmailAndCodeAsync(string email, string code, CancellationToken cancellationToken)
{
var normalizedEmail = email.ToLower().Trim();
var trimmedCode = code.Trim();
return await _context.ForgetPasswordCodes
.AsNoTracking()
.Where(u => u.Email.ToLower().Trim() == normalizedEmail && u.Code == trimmedCode)
.AnyAsync(cancellationToken);
}
public async Task<ForgetPasswordCode?> GetByEmailAsync(string email, CancellationToken cancellationToken)
{
var normalizedEmail = email.ToLower().Trim();
return await _context.ForgetPasswordCodes
.Where(u => u.Email.ToLower().Trim() == normalizedEmail)
.FirstOrDefaultAsync(cancellationToken);
}
public async Task RemoveByEmailAsync(string email, CancellationToken cancellationToken)
{
var normalizedEmail = email.ToLower().Trim();
var records = await _context.ForgetPasswordCodes
.Where(code => code.Email.ToLower().Trim() == normalizedEmail)
.ToListAsync(cancellationToken);
if (records.Count == 0)
return;
_context.ForgetPasswordCodes.RemoveRange(records);
await _context.SaveChangesAsync(cancellationToken);
}
}
}

View file

@ -1,5 +1,6 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Dto;
using SeaHaven.DataServices.Interfaces;
namespace SeaHaven.DataServices.Implementation
@ -33,5 +34,112 @@ namespace SeaHaven.DataServices.Implementation
{
return await _context.Users.AnyAsync(u => u.Id == id);
}
public async Task<IReadOnlyList<UserListRowData>> GetAllForListAsync(CancellationToken cancellationToken)
{
return await _context.Users
.AsNoTracking()
.Select(user => new UserListRowData
{
Id = user.Id,
RoleName = user.PhoneNumber,
Email = user.Email,
Name = user.FirstName,
CreatedDate = user.CreatedDate,
Status = user.UniqueName
})
.ToListAsync(cancellationToken);
}
public async Task<UserProfileData?> GetProfileAsync(string id, CancellationToken cancellationToken)
{
return await _context.Users
.AsNoTracking()
.Where(u => u.Id == id)
.Select(user => new UserProfileData
{
Id = user.Id,
FirstName = user.FirstName,
Email = user.Email,
Contact = user.Contact,
CreatedDate = user.CreatedDate
})
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<bool> UpdateProfileAsync(
string id,
string? name,
string? email,
string? contact,
CancellationToken cancellationToken)
{
var updated = await _context.Users
.Where(user => user.Id == id)
.ExecuteUpdateAsync(
setters => setters
.SetProperty(user => user.FirstName, name)
.SetProperty(user => user.Email, email)
.SetProperty(user => user.Contact, contact),
cancellationToken);
return updated == 1;
}
public async Task<ApplicationUser?> GetForEditAsync(string id, CancellationToken cancellationToken)
{
return await _context.Users
.Where(w => w.Id == id)
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<ApplicationUser?> GetByEmailNormalizedAsync(string email, CancellationToken cancellationToken)
{
var normalizedEmail = email.Trim().ToUpperInvariant();
return await _context.Users
.AsNoTracking()
.Where(user => user.NormalizedEmail == normalizedEmail)
.FirstOrDefaultAsync(cancellationToken);
}
public async Task UpdateUserAsync(ApplicationUser user, CancellationToken cancellationToken)
{
_context.Users.Update(user);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task DeleteUserWithCascadeAsync(ApplicationUser user, CancellationToken cancellationToken)
{
var id = user.Id;
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
await _context.workOrders
.Where(workOrder => workOrder.AssignTo == id)
.ExecuteUpdateAsync(
setters => setters.SetProperty(workOrder => workOrder.AssignTo, (string?)null),
cancellationToken);
await _context.Comments
.Where(comment => comment.UserId == id)
.ExecuteUpdateAsync(
setters => setters.SetProperty(comment => comment.UserId, (string?)null),
cancellationToken);
await _context.Templates
.Where(template => template.AssignTo == id)
.ExecuteUpdateAsync(
setters => setters.SetProperty(template => template.AssignTo, (string?)null),
cancellationToken);
await _context.UserRoles
.Where(role => role.UserId == id)
.ExecuteDeleteAsync(cancellationToken);
await _context.Users
.Where(existingUser => existingUser.Id == id)
.ExecuteDeleteAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
}
}

View file

@ -0,0 +1,13 @@
using Data.SeaHavenIndustries;
namespace SeaHaven.DataServices.Interfaces
{
public interface IForgetPasswordDataService
{
Task ReplaceCodeAsync(string email, string userId, string code, CancellationToken cancellationToken);
Task<bool> CodeExistsAsync(string code, CancellationToken cancellationToken);
Task<bool> ExistsByEmailAndCodeAsync(string email, string code, CancellationToken cancellationToken);
Task<ForgetPasswordCode?> GetByEmailAsync(string email, CancellationToken cancellationToken);
Task RemoveByEmailAsync(string email, CancellationToken cancellationToken);
}
}

View file

@ -1,4 +1,5 @@
using Data.SeaHavenIndustries;
using SeaHaven.DataServices.Dto;
namespace SeaHaven.DataServices.Interfaces
{
@ -8,5 +9,13 @@ namespace SeaHaven.DataServices.Interfaces
Task<IEnumerable<ApplicationUser>> GetAllAsync();
Task<ApplicationUser?> GetByEmailAsync(string email);
Task<bool> ExistsAsync(string id);
Task<IReadOnlyList<UserListRowData>> GetAllForListAsync(CancellationToken cancellationToken);
Task<UserProfileData?> GetProfileAsync(string id, CancellationToken cancellationToken);
Task<bool> UpdateProfileAsync(string id, string? name, string? email, string? contact, CancellationToken cancellationToken);
Task<ApplicationUser?> GetForEditAsync(string id, CancellationToken cancellationToken);
Task<ApplicationUser?> GetByEmailNormalizedAsync(string email, CancellationToken cancellationToken);
Task UpdateUserAsync(ApplicationUser user, CancellationToken cancellationToken);
Task DeleteUserWithCascadeAsync(ApplicationUser user, CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,68 @@
namespace SeaHaven.Services.DTOs
{
public class LoginResultDTO
{
public string Token { get; set; } = string.Empty;
public DateTime Expiration { get; set; }
public string? Email { get; set; }
public string? UserRole { get; set; }
public string? PhoneNumber { get; set; }
public string Fullname { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
}
public class UpdateProfileRequestDTO
{
public string? Name { get; set; }
public string? Email { get; set; }
public string? Contact { get; set; }
}
public class UserProfileDTO
{
public string? FirstName { get; set; }
public string? Email { get; set; }
public string? Contact { get; set; }
}
public class AddUserRequestDTO
{
public string? Name { get; set; }
public string? Email { get; set; }
public string? Contact { get; set; }
public string? Role { get; set; }
}
public class AddUserOutcomeDTO
{
public bool Success { get; set; }
public string? Error { get; set; }
}
public class EditUserRequestDTO
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Role { get; set; }
}
public class UserListRowDTO
{
public string? RoleName { get; set; }
public string? Email { get; set; }
public string? Name { get; set; }
public string Date { get; set; } = string.Empty;
public string? Status { get; set; }
public string? Id { get; set; }
}
public class UserProfileRowDTO
{
public string? Id { get; set; }
public string? Name { get; set; }
public DateTime? AddedDate { get; set; }
public string? Email { get; set; }
public string? Contact { get; set; }
}
}

View file

@ -0,0 +1,159 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
namespace SeaHaven.Services.Implementation
{
public class AuthenticationService : IAuthenticationService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IConfiguration _configuration;
private readonly IUserDataService _userDataService;
private readonly IForgetPasswordDataService _forgetPasswordDataService;
private readonly IEmailSender _emailSender;
public AuthenticationService(
UserManager<ApplicationUser> userManager,
IConfiguration configuration,
IUserDataService userDataService,
IForgetPasswordDataService forgetPasswordDataService,
IEmailSender emailSender)
{
_userManager = userManager;
_configuration = configuration;
_userDataService = userDataService;
_forgetPasswordDataService = forgetPasswordDataService;
_emailSender = emailSender;
}
public async Task<LoginResultDTO?> LoginAsync(string? username, string? password, CancellationToken cancellationToken)
{
var user = await _userManager.FindByNameAsync(username ?? "");
if (user != null && user.IsDeleted != true && await _userManager.CheckPasswordAsync(user, password ?? ""))
{
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName ?? ""),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var token = GetToken(authClaims);
return new LoginResultDTO
{
Token = new JwtSecurityTokenHandler().WriteToken(token),
Expiration = token.ValidTo,
Email = user.Email,
UserRole = userRoles.FirstOrDefault(),
PhoneNumber = user.PhoneNumber,
Fullname = user.FirstName + " " + user.LastName,
Id = user.Id
};
}
return null;
}
public async Task<bool> ChangePasswordAsync(string userId, string? currentPassword, string? confirmPassword, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return false;
var result = await _userManager.ChangePasswordAsync(user, currentPassword ?? "", confirmPassword ?? "");
return result.Succeeded;
}
public async Task<UserProfileDTO?> UpdateProfileAsync(string userId, UpdateProfileRequestDTO dto, CancellationToken cancellationToken)
{
var exists = await _userDataService.UpdateProfileAsync(
userId,
dto.Name,
dto.Email,
dto.Contact,
cancellationToken);
if (!exists)
return null;
var updated = await _userDataService.GetProfileAsync(userId, cancellationToken);
if (updated == null) return null;
return new UserProfileDTO
{
FirstName = updated.FirstName,
Email = updated.Email,
Contact = updated.Contact
};
}
public async Task<bool> ForgetPasswordAsync(string email, CancellationToken cancellationToken)
{
var user = await _userDataService.GetByEmailNormalizedAsync(email, cancellationToken);
if (user == null) return false;
var code = GenerateRandomNo();
await _forgetPasswordDataService.ReplaceCodeAsync(user.Email ?? "", user.Id, code, cancellationToken);
var body = $"Your Password Reset Code is: " + code;
await _emailSender.SendEmailAsync(user.Email ?? email, "Forget Password Request.", body);
return true;
}
public async Task<bool> VerifyCodeAsync(string code, CancellationToken cancellationToken)
{
return await _forgetPasswordDataService.CodeExistsAsync(code, cancellationToken);
}
public async Task<bool> ResetPasswordAsync(string email, string? code, string? password, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(password))
return false;
var matched = await _forgetPasswordDataService.ExistsByEmailAndCodeAsync(email, code, cancellationToken);
if (!matched) return false;
var record = await _forgetPasswordDataService.GetByEmailAsync(email, cancellationToken);
if (record == null)
return false;
var user = await _userManager.FindByIdAsync(record.UserId);
if (user == null)
return false;
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var result = await _userManager.ResetPasswordAsync(user, token, password);
if (!result.Succeeded)
return false;
await _forgetPasswordDataService.RemoveByEmailAsync(email, cancellationToken);
return true;
}
private JwtSecurityToken GetToken(List<Claim> authClaims)
{
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"] ?? ""));
var token = new JwtSecurityToken(
issuer: _configuration["JWT:ValidIssuer"],
audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddDays(10),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return token;
}
private static string GenerateRandomNo()
{
return RandomNumberGenerator.GetInt32(1_000_000).ToString("D6");
}
}
}

View file

@ -0,0 +1,201 @@
using Data.SeaHavenIndustries;
using Microsoft.AspNetCore.Identity;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
using System.Security.Cryptography;
namespace SeaHaven.Services.Implementation
{
public class UserService : IUserService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IUserDataService _userDataService;
private readonly IEmailSender _emailSender;
public UserService(
UserManager<ApplicationUser> userManager,
IUserDataService userDataService,
IEmailSender emailSender)
{
_userManager = userManager;
_userDataService = userDataService;
_emailSender = emailSender;
}
public async Task<IEnumerable<UserListRowDTO>> GetUsersAsync(CancellationToken cancellationToken)
{
var users = await _userDataService.GetAllForListAsync(cancellationToken);
return users.Select(s => new UserListRowDTO
{
RoleName = s.RoleName,
Email = s.Email,
Name = s.Name,
Date = s.CreatedDate != null ? s.CreatedDate.Value.Date.ToString("MMM dd yyyy") : "",
Status = s.Status,
Id = s.Id
});
}
public async Task<AddUserOutcomeDTO> AddUserAsync(AddUserRequestDTO dto, CancellationToken cancellationToken)
{
var model = new ApplicationUser
{
UserName = dto.Email,
FirstName = dto.Name,
Email = dto.Email,
};
var exist = await _userDataService.GetByIdAsync(model.Id);
if (exist == null)
{
model.EmailConfirmed = true;
model.UserName = model.Email;
model.CreatedDate = DateTime.Now;
model.UniqueName = "Active";
model.PhoneNumber = dto.Role;
var password = GenerateRandomPassword();
var result = await _userManager.CreateAsync(model, password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(model, dto.Role ?? "");
}
else
{
return new AddUserOutcomeDTO { Success = false, Error = result.Errors.FirstOrDefault()?.Description ?? "error" };
}
var domain = "http://shoc-ui-app.s3-website-us-east-1.amazonaws.com/#";
string subject = "Login Information";
string greeting = $"Dear {model.FirstName},\n\n";
string loginLink = $"Click here to login:<a href='{domain}/Login'>Login</a>\n";
string passwordInfo = $"Your password is: {password}\n\n";
string body = $"{greeting}\n\n{loginLink}\n\n{passwordInfo}";
await _emailSender.SendEmailAsync(model?.Email ?? "", subject, body);
return new AddUserOutcomeDTO { Success = true };
}
else
{
var exist1 = await _userDataService.GetForEditAsync(model.Id, cancellationToken);
if (exist1 == null)
return new AddUserOutcomeDTO { Success = false, Error = "User not found" };
var existingRole = await _userManager.GetRolesAsync(exist1);
if (existingRole != null && existingRole.Any() && existingRole.FirstOrDefault() != exist1.PhoneNumber)
{
await _userManager.RemoveFromRolesAsync(exist1, existingRole);
}
exist1.FirstName = model.FirstName;
exist1.LastName = model.LastName;
exist1.Contact = model.Contact;
exist1.PhoneNumber = model.PhoneNumber;
await _userDataService.UpdateUserAsync(exist1, cancellationToken);
await _userManager.AddToRoleAsync(exist1, dto.Role ?? "");
await _userManager.UpdateAsync(exist1);
return new AddUserOutcomeDTO { Success = true };
}
}
public async Task<bool> EditUserAsync(EditUserRequestDTO dto, CancellationToken cancellationToken)
{
var exist = await _userDataService.GetForEditAsync(dto.Id ?? "", cancellationToken);
if (exist == null)
return false;
exist.FirstName = dto.Name;
if (string.IsNullOrWhiteSpace(dto.Email))
throw new ArgumentException("Email is required.", nameof(dto));
exist.EmailConfirmed = true;
exist.UserName = dto.Email;
exist.Email = dto.Email;
exist.NormalizedEmail = dto.Email.ToUpperInvariant();
exist.NormalizedUserName = dto.Email.ToUpperInvariant();
exist.CreatedDate = DateTime.Now;
exist.UniqueName = "Active";
exist.PhoneNumber = dto.Role;
var existingRole = await _userManager.GetRolesAsync(exist);
if (existingRole != null && existingRole.Any())
{
await _userManager.RemoveFromRolesAsync(exist, existingRole);
}
await _userManager.AddToRoleAsync(exist, dto.Role ?? "");
await _userDataService.UpdateUserAsync(exist, cancellationToken);
return true;
}
public async Task<bool> DeleteUserAsync(string id, CancellationToken cancellationToken)
{
var data = await _userDataService.GetForEditAsync(id, cancellationToken);
if (data == null)
return false;
await _userDataService.DeleteUserWithCascadeAsync(data, cancellationToken);
return true;
}
public async Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken)
{
var exist = await _userDataService.GetForEditAsync(userId, cancellationToken);
if (exist != null)
{
exist.IsDeleted = true;
await _userDataService.UpdateUserAsync(exist, cancellationToken);
}
}
public async Task<IReadOnlyList<UserProfileRowDTO>> GetUserProfileAsync(string userId, CancellationToken cancellationToken)
{
var user = await _userDataService.GetProfileAsync(userId, cancellationToken);
if (user == null)
return Array.Empty<UserProfileRowDTO>();
return new List<UserProfileRowDTO>
{
new UserProfileRowDTO
{
Id = user.Id,
Name = user.FirstName,
AddedDate = user.CreatedDate,
Email = user.Email,
Contact = user.Contact
}
};
}
private static string GenerateRandomPassword(int length = 8)
{
const string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()_-+=<>?";
const string capitalLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const string digits = "1234567890";
const string symbols = "!@#$%^&*()_-+=<>?";
var passwordChars = new char[length];
passwordChars[0] = capitalLetters[RandomNumberGenerator.GetInt32(capitalLetters.Length)];
passwordChars[1] = digits[RandomNumberGenerator.GetInt32(digits.Length)];
passwordChars[2] = symbols[RandomNumberGenerator.GetInt32(symbols.Length)];
passwordChars[3] = "abcdefghijklmnopqrstuvwxyz"[RandomNumberGenerator.GetInt32(26)];
for (int i = 4; i < length; i++)
{
passwordChars[i] = validChars[RandomNumberGenerator.GetInt32(validChars.Length)];
}
for (int i = passwordChars.Length - 1; i > 0; i--)
{
int j = RandomNumberGenerator.GetInt32(i + 1);
(passwordChars[i], passwordChars[j]) = (passwordChars[j], passwordChars[i]);
}
return new string(passwordChars);
}
}
}

View file

@ -0,0 +1,14 @@
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Interfaces
{
public interface IAuthenticationService
{
Task<LoginResultDTO?> LoginAsync(string? username, string? password, CancellationToken cancellationToken);
Task<bool> ChangePasswordAsync(string userId, string? currentPassword, string? confirmPassword, CancellationToken cancellationToken);
Task<UserProfileDTO?> UpdateProfileAsync(string userId, UpdateProfileRequestDTO dto, CancellationToken cancellationToken);
Task<bool> ForgetPasswordAsync(string email, CancellationToken cancellationToken);
Task<bool> VerifyCodeAsync(string code, CancellationToken cancellationToken);
Task<bool> ResetPasswordAsync(string email, string? code, string? password, CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,11 @@
namespace SeaHaven.Services.Interfaces
{
/// <summary>
/// Outbound email port. Implemented in the API layer so the service layer
/// can orchestrate email without depending on the transport (SendGrid).
/// </summary>
public interface IEmailSender
{
Task<bool> SendEmailAsync(string emailTo, string subject, string body);
}
}

View file

@ -0,0 +1,14 @@
using SeaHaven.Services.DTOs;
namespace SeaHaven.Services.Interfaces
{
public interface IUserService
{
Task<IEnumerable<UserListRowDTO>> GetUsersAsync(CancellationToken cancellationToken);
Task<AddUserOutcomeDTO> AddUserAsync(AddUserRequestDTO dto, CancellationToken cancellationToken);
Task<bool> EditUserAsync(EditUserRequestDTO dto, CancellationToken cancellationToken);
Task<bool> DeleteUserAsync(string id, CancellationToken cancellationToken);
Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken);
Task<IReadOnlyList<UserProfileRowDTO>> GetUserProfileAsync(string userId, CancellationToken cancellationToken);
}
}