mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
refactor(identity): enforce service and data boundaries
This commit is contained in:
parent
445cd68468
commit
c440b2e3ee
20 changed files with 1637 additions and 317 deletions
231
Api.SeaHavenIndustries.Tests/AuthenticationControllerTests.cs
Normal file
231
Api.SeaHavenIndustries.Tests/AuthenticationControllerTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
172
Api.SeaHavenIndustries.Tests/AuthenticationServiceTests.cs
Normal file
172
Api.SeaHavenIndustries.Tests/AuthenticationServiceTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
42
Api.SeaHavenIndustries.Tests/IdentityTestHelpers.cs
Normal file
42
Api.SeaHavenIndustries.Tests/IdentityTestHelpers.cs
Normal 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
|
||||
};
|
||||
}
|
||||
190
Api.SeaHavenIndustries.Tests/UserControllerTests.cs
Normal file
190
Api.SeaHavenIndustries.Tests/UserControllerTests.cs
Normal 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\"");
|
||||
}
|
||||
}
|
||||
195
Api.SeaHavenIndustries.Tests/UserServiceTests.cs
Normal file
195
Api.SeaHavenIndustries.Tests/UserServiceTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
Api.SeaHavenIndustries/Helper/SendGridEmailSender.cs
Normal file
20
Api.SeaHavenIndustries/Helper/SendGridEmailSender.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 =>
|
||||
{
|
||||
|
|
|
|||
21
SeaHaven.DataServices/Dto/UserReadModels.cs
Normal file
21
SeaHaven.DataServices/Dto/UserReadModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
SeaHaven.Services/DTOs/IdentityDTOs.cs
Normal file
68
SeaHaven.Services/DTOs/IdentityDTOs.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
159
SeaHaven.Services/Implementation/AuthenticationService.cs
Normal file
159
SeaHaven.Services/Implementation/AuthenticationService.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
201
SeaHaven.Services/Implementation/UserService.cs
Normal file
201
SeaHaven.Services/Implementation/UserService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
SeaHaven.Services/Interfaces/IAuthenticationService.cs
Normal file
14
SeaHaven.Services/Interfaces/IAuthenticationService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
SeaHaven.Services/Interfaces/IEmailSender.cs
Normal file
11
SeaHaven.Services/Interfaces/IEmailSender.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
14
SeaHaven.Services/Interfaces/IUserService.cs
Normal file
14
SeaHaven.Services/Interfaces/IUserService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue