diff --git a/Server/ReasnAPI/ReasnAPI.Tests/ReasnAPI.Tests.csproj b/Server/ReasnAPI/ReasnAPI.Tests/ReasnAPI.Tests.csproj
index 1b46a3ed..852b4685 100644
--- a/Server/ReasnAPI/ReasnAPI.Tests/ReasnAPI.Tests.csproj
+++ b/Server/ReasnAPI/ReasnAPI.Tests/ReasnAPI.Tests.csproj
@@ -13,10 +13,15 @@
+
+
+
+
+
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTest1.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTest1.cs
deleted file mode 100644
index 46dac6f2..00000000
--- a/Server/ReasnAPI/ReasnAPI.Tests/UnitTest1.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace ReasnAPI.Tests
-{
- [TestClass]
- public class UnitTest1
- {
- [TestMethod]
- public void TestAdd()
- {
- Assert.AreEqual(17, 17);
- }
- }
-}
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ServiceExceptionHandlerTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ServiceExceptionHandlerTests.cs
new file mode 100644
index 00000000..15a71958
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ServiceExceptionHandlerTests.cs
@@ -0,0 +1,93 @@
+using System.Net;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using ReasnAPI.Exceptions;
+using ReasnAPI.Services.Exceptions;
+
+namespace ReasnAPI.Tests.UnitTests.Exceptions;
+
+[TestClass]
+public class ServiceExceptionHandlerTests
+{
+ private Mock _mockProblemDetailsService = null!;
+ private ServiceExceptionHandler _handler = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _mockProblemDetailsService = new Mock();
+ _handler = new ServiceExceptionHandler(_mockProblemDetailsService.Object);
+ }
+
+ [TestMethod]
+ public async Task HandleException_WhenBadRequestException_ShouldReturnProblemDetails()
+ {
+ var httpContext = new DefaultHttpContext();
+ var exception = new BadRequestException("Bad request");
+
+ ProblemDetailsContext? problemDetailsContext = null;
+ _mockProblemDetailsService.Setup(x =>
+ x.TryWriteAsync(It.IsAny()))
+ .Callback(context => problemDetailsContext = context)
+ .ReturnsAsync(true);
+
+ await _handler.TryHandleAsync(httpContext, exception, CancellationToken.None);
+
+ Assert.AreEqual((int)HttpStatusCode.BadRequest, httpContext.Response.StatusCode);
+ Assert.IsNotNull(problemDetailsContext);
+ Assert.AreEqual("https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
+ problemDetailsContext.ProblemDetails.Type);
+ Assert.AreEqual("A bad request was made",
+ problemDetailsContext.ProblemDetails.Title);
+ Assert.AreEqual(exception, problemDetailsContext.Exception);
+ Assert.AreEqual(exception.Message, problemDetailsContext.ProblemDetails.Detail);
+ }
+
+ [TestMethod]
+ public async Task HandleException_WhenNotFoundException_ShouldReturnProblemDetails()
+ {
+ var httpContext = new DefaultHttpContext();
+ var exception = new NotFoundException("Resource not found");
+
+ ProblemDetailsContext? problemDetailsContext = null;
+ _mockProblemDetailsService.Setup(x =>
+ x.TryWriteAsync(It.IsAny()))
+ .Callback(context => problemDetailsContext = context)
+ .ReturnsAsync(true);
+
+ await _handler.TryHandleAsync(httpContext, exception, CancellationToken.None);
+
+ Assert.AreEqual((int)HttpStatusCode.NotFound, httpContext.Response.StatusCode);
+ Assert.IsNotNull(problemDetailsContext);
+ Assert.AreEqual("https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4",
+ problemDetailsContext.ProblemDetails.Type);
+ Assert.AreEqual("A resource was not found",
+ problemDetailsContext.ProblemDetails.Title);
+ Assert.AreEqual(exception, problemDetailsContext.Exception);
+ Assert.AreEqual(exception.Message, problemDetailsContext.ProblemDetails.Detail);
+ }
+
+ [TestMethod]
+ public async Task HandleException_WhenVerificationException_ShouldReturnProblemDetails()
+ {
+ var httpContext = new DefaultHttpContext();
+ var exception = new VerificationException("Verification error");
+
+ ProblemDetailsContext? problemDetailsContext = null;
+ _mockProblemDetailsService.Setup(x =>
+ x.TryWriteAsync(It.IsAny()))
+ .Callback(context => problemDetailsContext = context)
+ .ReturnsAsync(true);
+
+ await _handler.TryHandleAsync(httpContext, exception, CancellationToken.None);
+
+ Assert.AreEqual((int)HttpStatusCode.BadRequest, httpContext.Response.StatusCode);
+ Assert.IsNotNull(problemDetailsContext);
+ Assert.AreEqual("https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
+ problemDetailsContext.ProblemDetails.Type);
+ Assert.AreEqual("A verification error occurred",
+ problemDetailsContext.ProblemDetails.Title);
+ Assert.AreEqual(exception, problemDetailsContext.Exception);
+ Assert.AreEqual(exception.Message, problemDetailsContext.ProblemDetails.Detail);
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ValidationExceptionHandlerTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ValidationExceptionHandlerTests.cs
new file mode 100644
index 00000000..9a087bb6
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ValidationExceptionHandlerTests.cs
@@ -0,0 +1,54 @@
+using System.Net;
+using FluentValidation;
+using FluentValidation.Results;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using ReasnAPI.Exceptions;
+
+namespace ReasnAPI.Tests.UnitTests.Exceptions;
+
+[TestClass]
+public class ValidationExceptionHandlerTests
+{
+ private Mock _mockProblemDetailsService = null!;
+ private ValidationExceptionHandler _handler = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _mockProblemDetailsService = new Mock();
+ _handler = new ValidationExceptionHandler(_mockProblemDetailsService.Object);
+ }
+
+ [TestMethod]
+ public async Task HandleException_WhenValidationException_ShouldReturnProblemDetails()
+ {
+ var httpContext = new DefaultHttpContext();
+ var exception = new ValidationException(new List
+ {
+ new ("Email", "'Email' must not be empty."),
+ new ("Password", "'Password' must not be empty.")
+ });
+
+ ProblemDetailsContext? problemDetailsContext = null;
+ _mockProblemDetailsService.Setup(x =>
+ x.TryWriteAsync(It.IsAny()))
+ .Callback(context => problemDetailsContext = context)
+ .ReturnsAsync(true);
+
+ await _handler.TryHandleAsync(httpContext, exception, CancellationToken.None);
+
+ Assert.AreEqual((int)HttpStatusCode.BadRequest, httpContext.Response.StatusCode);
+ Assert.IsNotNull(problemDetailsContext);
+ Assert.AreEqual("https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
+ problemDetailsContext.ProblemDetails.Type);
+ Assert.AreEqual("A validation error occurred",
+ problemDetailsContext.ProblemDetails.Title);
+ Assert.AreEqual(exception, problemDetailsContext.Exception);
+ Assert.AreEqual("One or more validation errors occurred",
+ problemDetailsContext.ProblemDetails.Detail);
+
+ Assert.IsNotNull(problemDetailsContext.ProblemDetails.Extensions);
+ Assert.IsTrue(problemDetailsContext.ProblemDetails.Extensions.ContainsKey("errors"));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs
new file mode 100644
index 00000000..631dbbde
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs
@@ -0,0 +1,125 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.WebUtilities;
+using Moq;
+using Moq.EntityFrameworkCore;
+using ReasnAPI.Models.Authentication;
+using ReasnAPI.Models.Database;
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Services.Authentication;
+using ReasnAPI.Services.Exceptions;
+
+namespace ReasnAPI.Tests.UnitTests.Services.Authentication;
+
+[TestClass]
+public class AuthServiceTests
+{
+ private Mock _mockContext = null!;
+ private PasswordHasher _hasher = null!;
+ private AuthService _service = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _mockContext = new Mock();
+ _hasher = new PasswordHasher();
+ _service = new AuthService(_mockContext.Object);
+
+ var user = new User
+ {
+ Email = "jon.snow@castleblack.com",
+ Username = "jsnow",
+ Password = _hasher.HashPassword(null!, "password")
+ };
+
+ _mockContext.Setup(c => c.Users)
+ .ReturnsDbSet(new List { user });
+ }
+
+ [TestMethod]
+ public void Login_WhenUserExistsAndPasswordIsCorrect_ShouldReturnUser()
+ {
+ var request = new LoginRequest
+ {
+ Email = "jon.snow@castleblack.com",
+ Password = "password"
+ };
+
+ var result = _service.Login(request);
+
+ Assert.IsNotNull(result);
+ Assert.IsInstanceOfType(result, typeof(User));
+ }
+
+ [TestMethod]
+ public void Login_WhenUserDoesNotExist_ShouldThrowNotFoundException()
+ {
+ var request = new LoginRequest
+ {
+ Email = "jon.notsnow@castleblack.com"
+ };
+
+ Assert.ThrowsException(() => _service.Login(request));
+ }
+
+ [TestMethod]
+ public void Login_WhenPasswordIsIncorrect_ShouldThrowVerificationException()
+ {
+ var request = new LoginRequest
+ {
+ Email = "jon.snow@castleblack.com",
+ Password = "wrong-password"
+ };
+
+ Assert.ThrowsException(() => _service.Login(request));
+ }
+
+ [TestMethod]
+ public void Register_WhenUserWithEmailAlreadyExists_ShouldThrowBadRequestException()
+ {
+ var request = new RegisterRequest
+ {
+ Email = "jon.snow@castleblack.com"
+ };
+
+ Assert.ThrowsException(() => _service.Register(request));
+ }
+
+ [TestMethod]
+ public void Register_WhenUserWithUsernameAlreadyExists_ShouldThrowBadRequestException()
+ {
+ var request = new RegisterRequest
+ {
+ Email = "jon.stark@castleblack.com",
+ Username = "jsnow"
+ };
+
+ Assert.ThrowsException(() => _service.Register(request));
+ }
+
+ [TestMethod]
+ public void Register_WhenUserDoesNotExist_ShouldReturnRegisteredUser()
+ {
+ var request = new RegisterRequest
+ {
+ Name = "Jon",
+ Surname = "Stark",
+ Email = "jon.stark@castleblack.com",
+ Username = "jstark",
+ Password = "S3cureP@ssword!",
+ Phone = "+123 456789",
+ Address = new AddressDto
+ {
+ Street = "The Wall",
+ City = "Castle Black",
+ Country = "Westeros",
+ State = "The North"
+ },
+ Role = "User"
+ };
+
+ var result = _service.Register(request);
+
+ Assert.IsNotNull(result);
+ Assert.IsInstanceOfType(result, typeof(User));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/TokenServiceTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/TokenServiceTests.cs
new file mode 100644
index 00000000..99e576fa
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/TokenServiceTests.cs
@@ -0,0 +1,83 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Cryptography;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using ReasnAPI.Models.Database;
+using ReasnAPI.Models.Enums;
+using ReasnAPI.Services.Authentication;
+
+namespace ReasnAPI.Tests.UnitTests.Services.Authentication;
+
+[TestClass]
+public class TokenServiceTests
+{
+ private const int DurationInHours = 8;
+ private const string IssAudValue = "http://localhost:5272";
+ private TokenService _service = null!;
+ private Mock _mockConfiguration = null!;
+ private User _validUser = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _mockConfiguration = new Mock();
+
+ var bytes = new byte[32];
+ RandomNumberGenerator.Fill(bytes);
+ _mockConfiguration.Setup(x =>
+ x["JwtSettings:Key"]).Returns(Convert.ToBase64String(bytes));
+
+ _mockConfiguration.Setup(x =>
+ x["JwtSettings:Issuer"]).Returns(IssAudValue);
+
+ var mockSection = new Mock();
+ var mockAudienceValue = new Mock();
+ mockAudienceValue.Setup(x => x.Value).Returns(IssAudValue);
+ mockSection.Setup(x =>
+ x.GetChildren()).Returns(new List { mockAudienceValue.Object });
+ _mockConfiguration.Setup(x =>
+ x.GetSection("JwtSettings:Audiences")).Returns(mockSection.Object);
+
+ var mockDurationValue = new Mock();
+ mockDurationValue.SetupGet(x => x.Value).Returns(DurationInHours.ToString());
+ _mockConfiguration.Setup(x =>
+ x.GetSection("JwtSettings:DurationInHours")).Returns(mockDurationValue.Object);
+
+ _service = new TokenService(_mockConfiguration.Object);
+
+ _validUser = new User
+ {
+ Id = 1,
+ Name = "Jon",
+ Surname = "Snow",
+ Email = "jon.snow@castleblack.com",
+ Role = UserRole.User
+ };
+ }
+
+ [TestMethod]
+ public void GenerateToken_WhenValidUser_ShouldReturnTokenPayload()
+ {
+ var result = _service.GenerateToken(_validUser);
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual("Bearer", result.TokenType);
+ Assert.IsNotNull(result.AccessToken);
+ Assert.AreEqual(DurationInHours * 60 * 60, result.ExpiresIn);
+ }
+
+ [TestMethod]
+ public void GenerateToken_WhenValidUser_ShouldReturnValidToken()
+ {
+ var result = _service.GenerateToken(_validUser);
+
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var token = tokenHandler.ReadToken(result.AccessToken) as JwtSecurityToken;
+
+ Assert.IsNotNull(token);
+ Assert.AreEqual(IssAudValue, token.Issuer);
+ Assert.AreEqual(IssAudValue, token.Audiences.First());
+ Assert.AreEqual(_validUser.Email, token.Subject);
+ Assert.AreEqual(DurationInHours * 60 * 60, (token.ValidTo - token.ValidFrom).TotalSeconds);
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/AddressValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/AddressValidatorTests.cs
new file mode 100644
index 00000000..c81d582c
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/AddressValidatorTests.cs
@@ -0,0 +1,261 @@
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Validators;
+
+namespace ReasnAPI.Tests.UnitTests.Validators;
+
+[TestClass]
+public class AddressValidatorTests
+{
+ private AddressValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new AddressValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenValidRequest_ShouldReturnTrue()
+ {
+ var request = new AddressDto
+ {
+ Street = "The Wall",
+ City = "Castle Black",
+ Country = "Westeros",
+ State = "The North",
+ ZipCode = "12345"
+ };
+ var result = _validator.Validate(request);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyStreet_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ Street = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Street' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenStreetTooLong_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ Street = new string('a', 65)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Street' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidStreet_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ Street = "The Wall!"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Street' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyCity_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ City = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'City' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenCityTooLong_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ City = new string('a', 65)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'City' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidCity_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ City = "Castle Black!"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'City' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyCountry_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ Country = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Country' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenCountryTooLong_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ Country = new string('a', 65)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "" +
+ "The length of 'Country' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidCountry_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ Country = "Westeros!"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Country' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyState_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ State = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'State' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenStateTooLong_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ State = new string('a', 65)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "" +
+ "The length of 'State' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidState_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ State = "The North!"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'State' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenZipCodeTooLong_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ ZipCode = new string('a', 9)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Zip Code' must be 8 characters or fewer. You entered 9 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidZipCode_ShouldReturnFalse()
+ {
+ var request = new AddressDto
+ {
+ ZipCode = "12345!"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Zip Code' is not in the correct format."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/Authentication/LoginRequestValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/Authentication/LoginRequestValidatorTests.cs
new file mode 100644
index 00000000..971de2cc
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/Authentication/LoginRequestValidatorTests.cs
@@ -0,0 +1,79 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using ReasnAPI.Models.Authentication;
+using ReasnAPI.Validators.Authentication;
+
+namespace ReasnAPI.Tests.UnitTests.Validators.Authentication;
+
+[TestClass]
+public class LoginRequestValidatorTests
+{
+ private LoginRequestValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new LoginRequestValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenValidRequest_ShouldReturnTrue()
+ {
+ var request = new LoginRequest
+ {
+ Email = "test@example.com",
+ Password = "password"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyEmail_ShouldReturnFalse()
+ {
+ var request = new LoginRequest
+ {
+ Email = "",
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Email' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidEmail_ShouldReturnFalse()
+ {
+ var request = new LoginRequest
+ {
+ Email = "invalid email"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Email' is not a valid email address."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyPassword_ShouldReturnFalse()
+ {
+ var request = new LoginRequest
+ {
+ Password = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Password' must not be empty."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/Authentication/RegisterRequestValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/Authentication/RegisterRequestValidatorTests.cs
new file mode 100644
index 00000000..c866983a
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/Authentication/RegisterRequestValidatorTests.cs
@@ -0,0 +1,339 @@
+using ReasnAPI.Validators.Authentication;
+using ReasnAPI.Models.Authentication;
+using ReasnAPI.Models.DTOs;
+
+namespace ReasnAPI.Tests.UnitTests.Validators.Authentication;
+
+[TestClass]
+public class RegisterRequestValidatorTests
+{
+ private RegisterRequestValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new RegisterRequestValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenValidRequest_ShouldReturnTrue()
+ {
+ var request = new RegisterRequest
+ {
+ Name = "Jon",
+ Surname = "Snow",
+ Email = "jon.snow@castleblack.com",
+ Username = "jonSnow",
+ Password = "S3cureP@ssword!",
+ Phone = "+123 456789",
+ Address = new AddressDto
+ {
+ Street = "The Wall",
+ City = "Castle Black",
+ Country = "Westeros",
+ State = "The North"
+ },
+ Role = "User"
+ };
+ var result = _validator.Validate(request);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyName_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Name = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameTooLong_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Name = new string('a', 65)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Console.WriteLine(result.Errors[0]);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Name' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidName_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Name = "123"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Console.WriteLine(result.Errors[0]);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptySurname_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Surname = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Surname' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenSurnameTooLong_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Surname = new string('a', 65)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Surname' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidSurname_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Surname = "123"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Surname' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyUsername_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Username = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Username' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenUsernameTooLong_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Username = new string('a', 65)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Username' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidUsername_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Username = "user name"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Username' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyEmail_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Email = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Email' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmailTooLong_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Email = new string('a', 256)
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Email' must be 255 characters or fewer. You entered 256 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidEmail_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Email = "invalid email"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Email' is not a valid email address."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyPassword_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Password = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Password' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidPassword_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Password = "password"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "Password must contain at least one uppercase letter, " +
+ "one lowercase letter, one number, and be at least 6 characters long."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidPhone_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Phone = "invalid phone"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Console.WriteLine(result.Errors[5]);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Phone' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyAddress_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Address = null!
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Address' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyRole_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Role = ""
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Role' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidRole_ShouldReturnFalse()
+ {
+ var request = new RegisterRequest
+ {
+ Role = "invalid role"
+ };
+
+ var result = _validator.Validate(request);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "Role must be either 'User' or 'Organizer'."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/CommentValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/CommentValidatorTests.cs
new file mode 100644
index 00000000..b1134ebf
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/CommentValidatorTests.cs
@@ -0,0 +1,63 @@
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Validators;
+
+namespace ReasnAPI.Tests.UnitTests.Validators;
+
+[TestClass]
+public class CommentValidatorTests
+{
+ private CommentValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new CommentValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenCommentIsValid_ShouldReturnTrue()
+ {
+ var comment = new CommentDto
+ {
+ Content = "Content",
+ CreatedAt = DateTime.UtcNow
+ };
+
+ var result = _validator.Validate(comment);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenContentIsEmpty_ShouldReturnFalse()
+ {
+ var comment = new CommentDto
+ {
+ Content = ""
+ };
+
+ var result = _validator.Validate(comment);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Content' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenContentIsTooLong_ShouldReturnFalse()
+ {
+ var comment = new CommentDto
+ {
+ Content = new string('a', 1025)
+ };
+
+ var result = _validator.Validate(comment);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Content' must be 1024 characters or fewer. You entered 1025 characters."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/EventValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/EventValidatorTests.cs
new file mode 100644
index 00000000..e7533a31
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/EventValidatorTests.cs
@@ -0,0 +1,208 @@
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Models.Enums;
+using ReasnAPI.Validators;
+
+namespace ReasnAPI.Tests.UnitTests.Validators;
+
+[TestClass]
+public class EventValidatorTests
+{
+ private EventValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new EventValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenEventIsValid_ShouldReturnTrue()
+ {
+ var eventDto = new EventDto
+ {
+ Name = "Event",
+ Description = "Description",
+ StartAt = DateTime.UtcNow,
+ EndAt = DateTime.UtcNow.AddDays(1),
+ CreatedAt = DateTime.UtcNow,
+ UpdatedAt = DateTime.UtcNow,
+ Slug = "the-slug",
+ Status = EventStatus.Approved
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameIsEmpty_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Name = ""
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameIsTooLong_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Name = new string('a', 65)
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Name' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenDescriptionIsEmpty_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Description = ""
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Description' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenDescriptionIsTooLong_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Description = new string('a', 4049)
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Description' must be 4048 characters or fewer. You entered 4049 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenStartAtIsAfterEndAt_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ StartAt = DateTime.UtcNow,
+ EndAt = DateTime.UtcNow.AddDays(-1)
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'StartAt' must be before 'EndAt'."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenSlugIsEmpty_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Slug = ""
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Slug' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenSlugIsTooLong_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Slug = new string('a', 129)
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Slug' must be 128 characters or fewer. You entered 129 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidSlug_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Slug = "the-slug!"
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Slug' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenTagsAreInvalid_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Tags = new List
+ {
+ new() { Name = "" }
+ }
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenParametersAreInvalid_ShouldReturnFalse()
+ {
+ var eventDto = new EventDto
+ {
+ Parameters = new List
+ {
+ new() { Key = "", Value = "" }
+ }
+ };
+
+ var result = _validator.Validate(eventDto);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Key' must not be empty."
+ ));
+ }
+
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/InterestValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/InterestValidatorTests.cs
new file mode 100644
index 00000000..8c7a2ba9
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/InterestValidatorTests.cs
@@ -0,0 +1,78 @@
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Validators;
+
+namespace ReasnAPI.Tests.UnitTests.Validators;
+
+[TestClass]
+public class InterestValidatorTests
+{
+ private InterestValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new InterestValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenInterestIsValid_ShouldReturnTrue()
+ {
+ var interest = new InterestDto
+ {
+ Name = "Interest"
+ };
+
+ var result = _validator.Validate(interest);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameIsEmpty_ShouldReturnFalse()
+ {
+ var interest = new InterestDto
+ {
+ Name = ""
+ };
+
+ var result = _validator.Validate(interest);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameIsTooLong_ShouldReturnFalse()
+ {
+ var interest = new InterestDto
+ {
+ Name = new string('a', 33)
+ };
+
+ var result = _validator.Validate(interest);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Name' must be 32 characters or fewer. You entered 33 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidName_ShouldReturnFalse()
+ {
+ var interest = new InterestDto
+ {
+ Name = "123"
+ };
+
+ var result = _validator.Validate(interest);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' is not in the correct format."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/ParameterValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/ParameterValidatorTests.cs
new file mode 100644
index 00000000..4f77c778
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/ParameterValidatorTests.cs
@@ -0,0 +1,128 @@
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Validators;
+
+namespace ReasnAPI.Tests.UnitTests.Validators;
+
+[TestClass]
+public class ParameterValidatorTests
+{
+ private ParameterValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new ParameterValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenParameterIsValid_ShouldReturnTrue()
+ {
+ var parameter = new ParameterDto
+ {
+ Key = "Key",
+ Value = "Value"
+ };
+
+ var result = _validator.Validate(parameter);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenKeyIsEmpty_ShouldReturnFalse()
+ {
+ var parameter = new ParameterDto
+ {
+ Key = ""
+ };
+
+ var result = _validator.Validate(parameter);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Key' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenKeyIsTooLong_ShouldReturnFalse()
+ {
+ var parameter = new ParameterDto
+ {
+ Key = new string('a', 33)
+ };
+
+ var result = _validator.Validate(parameter);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Key' must be 32 characters or fewer. You entered 33 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidKey_ShouldReturnFalse()
+ {
+ var parameter = new ParameterDto
+ {
+ Key = "123"
+ };
+
+ var result = _validator.Validate(parameter);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Key' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenValueIsEmpty_ShouldReturnFalse()
+ {
+ var parameter = new ParameterDto
+ {
+ Value = ""
+ };
+
+ var result = _validator.Validate(parameter);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Value' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenValueIsTooLong_ShouldReturnFalse()
+ {
+ var parameter = new ParameterDto
+ {
+ Value = new string('a', 65)
+ };
+
+ var result = _validator.Validate(parameter);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Value' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidValue_ShouldReturnFalse()
+ {
+ var parameter = new ParameterDto
+ {
+ Value = "Not a valid value!"
+ };
+
+ var result = _validator.Validate(parameter);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Value' is not in the correct format."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/TagValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/TagValidatorTests.cs
new file mode 100644
index 00000000..8f0097eb
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/TagValidatorTests.cs
@@ -0,0 +1,78 @@
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Validators;
+
+namespace ReasnAPI.Tests.UnitTests.Validators;
+
+[TestClass]
+public class TagValidatorTests
+{
+ private TagValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new TagValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenTagIsValid_ShouldReturnTrue()
+ {
+ var tag = new TagDto
+ {
+ Name = "Tag"
+ };
+
+ var result = _validator.Validate(tag);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameIsEmpty_ShouldReturnFalse()
+ {
+ var tag = new TagDto
+ {
+ Name = ""
+ };
+
+ var result = _validator.Validate(tag);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameIsTooLong_ShouldReturnFalse()
+ {
+ var tag = new TagDto
+ {
+ Name = new string('a', 65)
+ };
+
+ var result = _validator.Validate(tag);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Name' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameContainsInvalidCharacters_ShouldReturnFalse()
+ {
+ var tag = new TagDto
+ {
+ Name = "123"
+ };
+
+ var result = _validator.Validate(tag);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' is not in the correct format."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/UserValidatorTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/UserValidatorTests.cs
new file mode 100644
index 00000000..508ea1ef
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Validators/UserValidatorTests.cs
@@ -0,0 +1,247 @@
+using ReasnAPI.Models.DTOs;
+using ReasnAPI.Models.Enums;
+using ReasnAPI.Validators;
+
+namespace ReasnAPI.Tests.UnitTests.Validators;
+
+[TestClass]
+public class UserValidatorTests
+{
+ private UserValidator _validator = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _validator = new UserValidator();
+ }
+
+ [TestMethod]
+ public void Validate_WhenValidRequest_ShouldReturnTrue()
+ {
+ var user = new UserDto
+ {
+ Name = "Jon",
+ Surname = "Snow",
+ Email = "jon.snow@castleblack.com",
+ Username = "jonSnow",
+ Phone = "+123 456789",
+ Role = UserRole.User
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyName_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Name = ""
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenNameTooLong_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Name = new string('a', 65)
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Name' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidName_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Name = "123"
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Name' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptySurname_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Surname = ""
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Surname' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenSurnameTooLong_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Surname = new string('a', 65)
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Surname' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidSurname_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Surname = "123"
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Surname' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyEmail_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Email = ""
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Email' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmailTooLong_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Email = new string('a', 256)
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Email' must be 255 characters or fewer. You entered 256 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidEmail_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Email = "jon.snow"
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Email' is not a valid email address."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenEmptyUsername_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Username = ""
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Username' must not be empty."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenUsernameTooLong_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Username = new string('a', 65)
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage ==
+ "The length of 'Username' must be 64 characters or fewer. You entered 65 characters."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidUsername_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Username = "user name"
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Username' is not in the correct format."
+ ));
+ }
+
+ [TestMethod]
+ public void Validate_WhenInvalidPhone_ShouldReturnFalse()
+ {
+ var user = new UserDto
+ {
+ Phone = "123"
+ };
+
+ var result = _validator.Validate(user);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Errors.Exists(
+ e => e.ErrorMessage == "'Phone' is not in the correct format."
+ ));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Common/IAssemblyMarker.cs b/Server/ReasnAPI/ReasnAPI/Common/IAssemblyMarker.cs
new file mode 100644
index 00000000..916dc1d9
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Common/IAssemblyMarker.cs
@@ -0,0 +1,6 @@
+namespace ReasnAPI.Validators;
+
+public interface IAssemblyMarker
+{
+
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/.gitkeep b/Server/ReasnAPI/ReasnAPI/Controllers/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/AuthController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/AuthController.cs
new file mode 100644
index 00000000..9a73f067
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Controllers/AuthController.cs
@@ -0,0 +1,49 @@
+using FluentValidation;
+using Microsoft.AspNetCore.Mvc;
+using ReasnAPI.Mappers;
+using ReasnAPI.Models.Authentication;
+using ReasnAPI.Services.Authentication;
+
+namespace ReasnAPI.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class AuthController : ControllerBase
+{
+ private readonly AuthService _authService;
+ private readonly TokenService _tokenService;
+
+ public AuthController(AuthService authService, TokenService tokenService)
+ {
+ _authService = authService;
+ _tokenService = tokenService;
+ }
+
+ [HttpPost("login")]
+ public IActionResult Login(
+ [FromBody] LoginRequest request,
+ [FromServices] IValidator validator)
+ {
+ validator.ValidateAndThrow(request);
+ var user = _authService.Login(request);
+
+ var tokenPayload = _tokenService.GenerateToken(user);
+ return Ok(tokenPayload);
+ }
+
+ [HttpPost("register")]
+ public IActionResult Register(
+ [FromBody] RegisterRequest request,
+ [FromServices] IValidator validator)
+ {
+ validator.ValidateAndThrow(request);
+ var user = _authService.Register(request);
+
+ var location = Url.Action(
+ action: nameof(UsersController.GetUserByUsername),
+ controller: "Users",
+ values: new { username = user.Username });
+
+ return Created(location, user.ToDto());
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs
new file mode 100644
index 00000000..0904a8bf
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace ReasnAPI.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class UsersController : ControllerBase
+{
+ [HttpGet]
+ [Route("{username}")]
+ public IActionResult GetUserByUsername(string username)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Exceptions/ServiceExceptionHandler.cs b/Server/ReasnAPI/ReasnAPI/Exceptions/ServiceExceptionHandler.cs
new file mode 100644
index 00000000..fd76c213
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Exceptions/ServiceExceptionHandler.cs
@@ -0,0 +1,58 @@
+using System.Net;
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using ReasnAPI.Services.Exceptions;
+
+namespace ReasnAPI.Exceptions;
+
+public class ServiceExceptionHandler : IExceptionHandler
+{
+ private readonly IProblemDetailsService _problemDetailsService;
+
+ public ServiceExceptionHandler(IProblemDetailsService problemDetailsService)
+ {
+ _problemDetailsService = problemDetailsService;
+ }
+
+ public async ValueTask TryHandleAsync(
+ HttpContext httpContext,
+ Exception exception,
+ CancellationToken cancellationToken)
+ {
+ var problemDetails = new ProblemDetails
+ {
+ Detail = exception.Message
+ };
+
+ switch (exception)
+ {
+ case BadRequestException:
+ httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1";
+ problemDetails.Title = "A bad request was made";
+ break;
+
+ case NotFoundException:
+ httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4";
+ problemDetails.Title = "A resource was not found";
+ break;
+
+ case VerificationException:
+ httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1";
+ problemDetails.Title = "A verification error occurred";
+ break;
+
+ default:
+ return false;
+ }
+
+ return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
+ {
+ HttpContext = httpContext,
+ ProblemDetails = problemDetails,
+ Exception = exception
+ });
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Exceptions/ValidationExceptionHandler.cs b/Server/ReasnAPI/ReasnAPI/Exceptions/ValidationExceptionHandler.cs
new file mode 100644
index 00000000..8cbc68c9
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Exceptions/ValidationExceptionHandler.cs
@@ -0,0 +1,49 @@
+using System.Net;
+using FluentValidation;
+using Microsoft.AspNetCore.Diagnostics;
+
+namespace ReasnAPI.Exceptions;
+
+public class ValidationExceptionHandler : IExceptionHandler
+{
+ private readonly IProblemDetailsService _problemDetailsService;
+
+ public ValidationExceptionHandler(IProblemDetailsService problemDetailsService)
+ {
+ _problemDetailsService = problemDetailsService;
+ }
+
+ public async ValueTask TryHandleAsync(
+ HttpContext httpContext,
+ Exception exception,
+ CancellationToken cancellationToken)
+ {
+ if (exception is not ValidationException validationException)
+ {
+ return false;
+ }
+
+ var errors = validationException.Errors.Select(e => new
+ {
+ field = e.PropertyName,
+ message = e.ErrorMessage
+ }).ToList();
+
+ httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ return await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
+ {
+ HttpContext = httpContext,
+ ProblemDetails =
+ {
+ Title = "A validation error occurred",
+ Detail = "One or more validation errors occurred",
+ Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
+ Extensions =
+ {
+ ["errors"] = errors
+ }
+ },
+ Exception = validationException
+ });
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/AddressMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/AddressMapper.cs
new file mode 100644
index 00000000..f07e575d
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Mappers/AddressMapper.cs
@@ -0,0 +1,23 @@
+using ReasnAPI.Models.Database;
+using ReasnAPI.Models.DTOs;
+
+namespace ReasnAPI.Mappers;
+
+public static class AddressMapper
+{
+ public static AddressDto ToDto(this Address address)
+ {
+ return new AddressDto
+ {
+ Street = address.Street,
+ City = address.City,
+ State = address.State,
+ ZipCode = address.ZipCode
+ };
+ }
+
+ public static List ToDtoList(this IEnumerable addresses)
+ {
+ return addresses.Select(ToDto).ToList();
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/InterestMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/InterestMapper.cs
new file mode 100644
index 00000000..c8e2fe3f
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Mappers/InterestMapper.cs
@@ -0,0 +1,20 @@
+using ReasnAPI.Models.Database;
+using ReasnAPI.Models.DTOs;
+
+namespace ReasnAPI.Mappers;
+
+public static class InterestMapper
+{
+ public static InterestDto ToDto(this Interest interest)
+ {
+ return new InterestDto
+ {
+ Name = interest.Name
+ };
+ }
+
+ public static List ToDtoList(this IEnumerable interests)
+ {
+ return interests.Select(ToDto).ToList();
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs
new file mode 100644
index 00000000..ae9bdf0a
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs
@@ -0,0 +1,21 @@
+using ReasnAPI.Models.Database;
+using ReasnAPI.Models.DTOs;
+
+namespace ReasnAPI.Mappers;
+
+public static class UserInterestMapper
+{
+ public static UserInterestDto ToDto(this UserInterest userInterest)
+ {
+ return new UserInterestDto
+ {
+ Interest = userInterest.Interest.ToDto(),
+ Level = userInterest.Level
+ };
+ }
+
+ public static List ToDtoList(this IEnumerable userInterests)
+ {
+ return userInterests.Select(ToDto).ToList();
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/UserMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/UserMapper.cs
new file mode 100644
index 00000000..512756b8
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Mappers/UserMapper.cs
@@ -0,0 +1,27 @@
+using ReasnAPI.Models.Database;
+using ReasnAPI.Models.DTOs;
+
+namespace ReasnAPI.Mappers;
+
+public static class UserMapper
+{
+ public static UserDto ToDto(this User user)
+ {
+ return new UserDto
+ {
+ Username = user.Username,
+ Name = user.Name,
+ Surname = user.Surname,
+ Email = user.Email,
+ Phone = user.Phone,
+ Role = user.Role,
+ AddressId = user.AddressId,
+ Interests = user.UserInterests.ToDtoList()
+ };
+ }
+
+ public static List ToDtoList(this IEnumerable users)
+ {
+ return users.Select(ToDto).ToList();
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Models/Authentication/LoginRequest.cs b/Server/ReasnAPI/ReasnAPI/Models/Authentication/LoginRequest.cs
new file mode 100644
index 00000000..addc4f34
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Models/Authentication/LoginRequest.cs
@@ -0,0 +1,7 @@
+namespace ReasnAPI.Models.Authentication;
+
+public class LoginRequest
+{
+ public string Email { get; set; } = null!;
+ public string Password { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Models/Authentication/RegisterRequest.cs b/Server/ReasnAPI/ReasnAPI/Models/Authentication/RegisterRequest.cs
new file mode 100644
index 00000000..cbcbb81a
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Models/Authentication/RegisterRequest.cs
@@ -0,0 +1,15 @@
+using ReasnAPI.Models.DTOs;
+
+namespace ReasnAPI.Models.Authentication;
+
+public class RegisterRequest
+{
+ public string Name { get; set; } = null!;
+ public string Surname { get; set; } = null!;
+ public string Email { get; set; } = null!;
+ public string Username { get; set; } = null!;
+ public string Password { get; set; } = null!;
+ public string? Phone { get; set; }
+ public AddressDto Address { get; set; } = null!;
+ public string Role { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Models/Authentication/TokenPayload.cs b/Server/ReasnAPI/ReasnAPI/Models/Authentication/TokenPayload.cs
new file mode 100644
index 00000000..118726d6
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Models/Authentication/TokenPayload.cs
@@ -0,0 +1,8 @@
+namespace ReasnAPI.Models.Authentication;
+
+public class TokenPayload
+{
+ public string TokenType { get; set; }
+ public string AccessToken { get; set; }
+ public int ExpiresIn { get; set; }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Models/Database/ReasnContext.cs b/Server/ReasnAPI/ReasnAPI/Models/Database/ReasnContext.cs
index 98c62d12..1656e6b0 100644
--- a/Server/ReasnAPI/ReasnAPI/Models/Database/ReasnContext.cs
+++ b/Server/ReasnAPI/ReasnAPI/Models/Database/ReasnContext.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using ReasnAPI.Models.Enums;
namespace ReasnAPI.Models.Database;
@@ -37,7 +38,12 @@ public ReasnContext(DbContextOptions options)
public virtual DbSet UserInterests { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- => optionsBuilder.UseNpgsql("name=ConnectionStrings:DefaultValue");
+ {
+ if (!optionsBuilder.IsConfigured)
+ {
+ optionsBuilder.UseNpgsql("name=ConnectionStrings:DefaultValue");
+ }
+ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -45,28 +51,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasPostgresEnum("common", "event_status", new[] { "Completed", "In progress", "Approved", "Waiting for approval" })
.HasPostgresEnum("common", "object_type", new[] { "Event", "User" })
.HasPostgresEnum("common", "participant_status", new[] { "Interested", "Participating" })
- .HasPostgresEnum("users", "role", new[] { "User", "Organizer", "Admin" });
-
- modelBuilder
- .Entity()
- .Property(u => u.Role)
- .HasConversion();
-
- modelBuilder
- .Entity()
- .Property(u => u.Status)
- .HasConversion();
-
- modelBuilder
- .Entity()
- .Property(u => u.ObjectType)
- .HasConversion();
-
- modelBuilder
- .Entity()
- .Property(u => u.Status)
- .HasConversion();
-
+ .HasPostgresEnum("users", "role", new[] { "User", "Organizer", "Admin" });
+
modelBuilder.Entity(entity =>
{
entity.HasKey(e => e.Id).HasName("address_pkey");
diff --git a/Server/ReasnAPI/ReasnAPI/Models/Enums/UserRole.cs b/Server/ReasnAPI/ReasnAPI/Models/Enums/UserRole.cs
index 4a35be42..9b22fe02 100644
--- a/Server/ReasnAPI/ReasnAPI/Models/Enums/UserRole.cs
+++ b/Server/ReasnAPI/ReasnAPI/Models/Enums/UserRole.cs
@@ -1,8 +1,15 @@
-namespace ReasnAPI.Models.Enums;
+using NpgsqlTypes;
+
+namespace ReasnAPI.Models.Enums;
public enum UserRole
{
- User,
- Organizer,
+ [PgName("User")]
+ User,
+
+ [PgName("Organizer")]
+ Organizer,
+
+ [PgName("Admin")]
Admin
}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Program.cs b/Server/ReasnAPI/ReasnAPI/Program.cs
index 67d181aa..4bb3484c 100644
--- a/Server/ReasnAPI/ReasnAPI/Program.cs
+++ b/Server/ReasnAPI/ReasnAPI/Program.cs
@@ -1,26 +1,69 @@
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Npgsql;
using ReasnAPI.Models.Enums;
using Serilog;
-using System;
+using System.Text;
using System.Text.Json.Serialization;
+using FluentValidation;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+using ReasnAPI.Exceptions;
using ReasnAPI.Models.Database;
+using ReasnAPI.Services.Authentication;
+using ReasnAPI.Validators;
var builder = WebApplication.CreateSlimBuilder(args);
+var config = builder.Configuration;
-builder.Services.AddControllers();
-var dataSourceBuilder = new NpgsqlDataSourceBuilder(builder.Configuration.GetConnectionString("DefaultValue"));
-dataSourceBuilder.MapEnum("events.participant.status");
-dataSourceBuilder.MapEnum("events.event.status");
-dataSourceBuilder.MapEnum("common.image.object_type");
-dataSourceBuilder.MapEnum("users.user.role");
+builder.Services.AddAuthentication(options =>
+{
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+}).AddJwtBearer(jwt =>
+{
+ jwt.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidIssuer = config["JwtSettings:Issuer"],
+ ValidAudiences = config.GetSection("JwtSettings:Audiences").Get>(),
+ IssuerSigningKey = new SymmetricSecurityKey
+ (Encoding.UTF8.GetBytes(config["JwtSettings:Key"]!)),
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true
+ };
+});
+
+builder.Services.AddExceptionHandler();
+builder.Services.AddExceptionHandler();
+builder.Services.AddProblemDetails(options =>
+ options.CustomizeProblemDetails = ctx =>
+ {
+ ctx.ProblemDetails.Instance = $"{ctx.HttpContext.Request.Method} {ctx.HttpContext.Request.Path}";
+ });
+
+builder.Services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+ });
+
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddValidatorsFromAssemblyContaining();
+
+var dataSourceBuilder = new NpgsqlDataSourceBuilder(config.GetConnectionString("DefaultValue"));
+dataSourceBuilder.MapEnum("common.participant_status");
+dataSourceBuilder.MapEnum("common.event_status");
+dataSourceBuilder.MapEnum("common.object_type");
+dataSourceBuilder.MapEnum("users.role");
var dataSource = dataSourceBuilder.Build();
-// todo: uncomment after creating DbContext and change context name and if needed - connection string localized in appsettings.json
builder.Services.AddDbContext(options =>
- options.UseNpgsql(dataSource));
+ options.UseNpgsql(dataSource)
+ .EnableDetailedErrors());
builder.Services.AddSwaggerGen(options =>
{
@@ -29,10 +72,32 @@
Version = "v1",
Title = "Reasn API"
});
+ options.AddSecurityDefinition("JWT", new OpenApiSecurityScheme
+ {
+ Description = "JWT Authorization header using the Bearer scheme.",
+ Name = "Authorization",
+ In = ParameterLocation.Header,
+ Type = SecuritySchemeType.ApiKey,
+ Scheme = "Bearer"
+ });
+ options.AddSecurityRequirement(new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = "JWT"
+ }
+ },
+ Array.Empty()
+ }
+ });
});
Log.Logger = new LoggerConfiguration()
- .ReadFrom.Configuration(builder.Configuration).CreateLogger();
+ .ReadFrom.Configuration(config).CreateLogger();
builder.Host.UseSerilog();
@@ -47,10 +112,16 @@
});
}
+app.UseExceptionHandler();
+app.UseStatusCodePages();
+
app.UseSerilogRequestLogging();
app.UseHttpsRedirection();
+app.UseAuthentication();
+app.UseAuthorization();
+
app.MapSwagger();
app.MapControllers();
diff --git a/Server/ReasnAPI/ReasnAPI/ReasnAPI.csproj b/Server/ReasnAPI/ReasnAPI/ReasnAPI.csproj
index 23499ae8..312bd617 100644
--- a/Server/ReasnAPI/ReasnAPI/ReasnAPI.csproj
+++ b/Server/ReasnAPI/ReasnAPI/ReasnAPI.csproj
@@ -9,23 +9,17 @@
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
-
-
-
-
-
-
diff --git a/Server/ReasnAPI/ReasnAPI/Services/.gitkeep b/Server/ReasnAPI/ReasnAPI/Services/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/Server/ReasnAPI/ReasnAPI/Services/Authentication/AuthService.cs b/Server/ReasnAPI/ReasnAPI/Services/Authentication/AuthService.cs
new file mode 100644
index 00000000..1d66a426
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Services/Authentication/AuthService.cs
@@ -0,0 +1,79 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using ReasnAPI.Models.Authentication;
+using ReasnAPI.Models.Database;
+using ReasnAPI.Models.Enums;
+using ReasnAPI.Services.Exceptions;
+
+namespace ReasnAPI.Services.Authentication;
+
+public class AuthService
+{
+ private readonly ReasnContext _context;
+ private readonly PasswordHasher _hasher;
+
+ public AuthService(ReasnContext context)
+ {
+ _context = context;
+ _hasher = new PasswordHasher();
+ }
+
+ public User Login(LoginRequest request)
+ {
+ var user = _context.Users.FirstOrDefault(u =>
+ u.Email.ToUpper() == request.Email.ToUpper());
+
+ if (user is null)
+ {
+ throw new NotFoundException("Not found user related with provided email");
+ }
+
+ var result = _hasher.VerifyHashedPassword(
+ user, user.Password, request.Password);
+
+ if (result != PasswordVerificationResult.Success)
+ {
+ throw new VerificationException("Provided password is incorrect");
+ }
+
+ return user;
+ }
+
+ public User Register(RegisterRequest request)
+ {
+ var userAlreadyExists = _context.Users.Any(u =>
+ u.Email.ToUpper() == request.Email.ToUpper() ||
+ u.Username.ToUpper() == request.Username.ToUpper());
+
+ if (userAlreadyExists)
+ {
+ throw new BadRequestException(
+ "User with provided email or username already exists");
+ }
+
+ var user = new User
+ {
+ Name = request.Name,
+ Surname = request.Surname,
+ Email = request.Email,
+ Username = request.Username,
+ Role = Enum.Parse(request.Role),
+ IsActive = true,
+ CreatedAt = DateTime.UtcNow,
+ UpdatedAt = DateTime.UtcNow,
+ Address = new Address
+ {
+ Country = request.Address.Country,
+ City = request.Address.City,
+ Street = request.Address.Street,
+ State = request.Address.State,
+ }
+ };
+ _context.Users.Add(user);
+
+ user.Password = _hasher.HashPassword(user, request.Password);
+ _context.SaveChanges();
+
+ return user;
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Services/Authentication/TokenService.cs b/Server/ReasnAPI/ReasnAPI/Services/Authentication/TokenService.cs
new file mode 100644
index 00000000..e6752045
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Services/Authentication/TokenService.cs
@@ -0,0 +1,53 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using Microsoft.IdentityModel.Tokens;
+using ReasnAPI.Models.Authentication;
+using ReasnAPI.Models.Database;
+
+namespace ReasnAPI.Services.Authentication;
+
+public class TokenService
+{
+ private readonly IConfiguration _configuration;
+
+ public TokenService(IConfiguration configuration) =>
+ _configuration = configuration;
+
+ public TokenPayload GenerateToken(User user)
+ {
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var tokenLifetime = TimeSpan.FromHours(
+ _configuration.GetValue("JwtSettings:DurationInHours"));
+
+ var key = Encoding.UTF8.GetBytes(_configuration["JwtSettings:Key"]!);
+ var issuer = _configuration["JwtSettings:Issuer"]!;
+ var audiences = _configuration.GetSection("JwtSettings:Audiences")
+ .Get>()!;
+
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Subject = new ClaimsIdentity(new[]
+ {
+ new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
+ new Claim(JwtRegisteredClaimNames.Sub, user.Email),
+ new Claim(JwtRegisteredClaimNames.Email, user.Email),
+ new Claim(ClaimTypes.Role, user.Role.ToString()),
+ new Claim(JwtRegisteredClaimNames.Iss, issuer),
+ new Claim(JwtRegisteredClaimNames.Aud, string.Join(",", audiences))
+ }),
+ Expires = DateTime.UtcNow.Add(tokenLifetime),
+ SigningCredentials = new SigningCredentials(
+ new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
+ };
+
+ var token = tokenHandler.CreateToken(tokenDescriptor);
+ var tokenPayload = new TokenPayload
+ {
+ TokenType = "Bearer",
+ AccessToken = tokenHandler.WriteToken(token),
+ ExpiresIn = Convert.ToInt32(tokenLifetime.TotalSeconds)
+ };
+ return tokenPayload;
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Services/Exceptions/BadRequestException.cs b/Server/ReasnAPI/ReasnAPI/Services/Exceptions/BadRequestException.cs
new file mode 100644
index 00000000..7b630bda
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Services/Exceptions/BadRequestException.cs
@@ -0,0 +1,10 @@
+namespace ReasnAPI.Services.Exceptions;
+
+public class BadRequestException : Exception
+{
+ public BadRequestException() { }
+
+ public BadRequestException(string message) : base(message) { }
+
+ public BadRequestException(string message, Exception inner) : base(message, inner) { }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Services/Exceptions/NotFoundException.cs b/Server/ReasnAPI/ReasnAPI/Services/Exceptions/NotFoundException.cs
new file mode 100644
index 00000000..781b52b9
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Services/Exceptions/NotFoundException.cs
@@ -0,0 +1,10 @@
+namespace ReasnAPI.Services.Exceptions;
+
+public class NotFoundException : Exception
+{
+ public NotFoundException() { }
+
+ public NotFoundException(string message) : base(message) { }
+
+ public NotFoundException(string message, Exception inner) : base(message, inner) { }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Services/Exceptions/VerificationException.cs b/Server/ReasnAPI/ReasnAPI/Services/Exceptions/VerificationException.cs
new file mode 100644
index 00000000..cd5a1749
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Services/Exceptions/VerificationException.cs
@@ -0,0 +1,10 @@
+namespace ReasnAPI.Services.Exceptions;
+
+public class VerificationException : Exception
+{
+ public VerificationException() { }
+
+ public VerificationException(string message) : base(message) { }
+
+ public VerificationException(string message, Exception inner) : base(message, inner) { }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/.gitkeep b/Server/ReasnAPI/ReasnAPI/Validators/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/AddressValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/AddressValidator.cs
new file mode 100644
index 00000000..6f2b2efb
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/AddressValidator.cs
@@ -0,0 +1,47 @@
+using FluentValidation;
+using ReasnAPI.Models.DTOs;
+
+namespace ReasnAPI.Validators;
+
+public class AddressValidator : AbstractValidator
+{
+ private const int MaxCountryLength = 64;
+ private const int MaxCityLength = 64;
+ private const int MaxStreetLength = 64;
+ private const int MaxStateLength = 64;
+ private const int MaxZipCodeLength = 8;
+
+ private const string CountryRegex = @"^\p{Lu}[\p{L}\s'-]*(? a.Country)
+ .NotEmpty()
+ .MaximumLength(MaxCountryLength)
+ .Matches(CountryRegex);
+
+ RuleFor(a => a.City)
+ .NotEmpty()
+ .MaximumLength(MaxCityLength)
+ .Matches(CityRegex);
+
+ RuleFor(a => a.Street)
+ .NotEmpty()
+ .MaximumLength(MaxStreetLength)
+ .Matches(StreetRegex);
+
+ RuleFor(a => a.State)
+ .NotEmpty()
+ .MaximumLength(MaxStateLength)
+ .Matches(StateRegex);
+
+ RuleFor(r => r.ZipCode)
+ .MaximumLength(MaxZipCodeLength)
+ .Matches(ZipCodeRegex)
+ .When(r => !string.IsNullOrEmpty(r.ZipCode));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/Authentication/LoginRequestValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/Authentication/LoginRequestValidator.cs
new file mode 100644
index 00000000..2fdc9c99
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/Authentication/LoginRequestValidator.cs
@@ -0,0 +1,17 @@
+using FluentValidation;
+using ReasnAPI.Models.Authentication;
+
+namespace ReasnAPI.Validators.Authentication;
+
+public class LoginRequestValidator : AbstractValidator
+{
+ public LoginRequestValidator()
+ {
+ RuleFor(lr => lr.Email)
+ .NotEmpty()
+ .EmailAddress();
+
+ RuleFor(lr => lr.Password)
+ .NotEmpty();
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/Authentication/RegisterRequestValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/Authentication/RegisterRequestValidator.cs
new file mode 100644
index 00000000..255dfc93
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/Authentication/RegisterRequestValidator.cs
@@ -0,0 +1,62 @@
+using FluentValidation;
+using ReasnAPI.Models.Authentication;
+using ReasnAPI.Models.Enums;
+
+namespace ReasnAPI.Validators.Authentication;
+
+public class RegisterRequestValidator : AbstractValidator
+{
+ private const int MaxNameLength = 64;
+ private const int MaxSurnameLength = 64;
+ private const int MaxUsernameLength = 64;
+ private const int MaxEmailLength = 255;
+
+ private const string NameRegex = @"^\p{Lu}[\p{Ll}\s'-]+$";
+ private const string SurnameRegex = @"^\p{L}+(?:[\s'-]\p{L}+)*$";
+ private const string UsernameRegex = @"^[\p{L}\d._%+-]{4,}$";
+ private const string PasswordRegex = @"^((?=\S*?[A-Z])(?=\S*?[a-z])(?=\S*?[0-9]).{6,})\S$";
+ private const string PhoneRegex = @"^\+\d{1,3}\s\d{1,15}$";
+
+ public RegisterRequestValidator()
+ {
+ RuleFor(r => r.Name)
+ .NotEmpty()
+ .MaximumLength(MaxNameLength)
+ .Matches(NameRegex);
+
+ RuleFor(r => r.Surname)
+ .NotEmpty()
+ .MaximumLength(MaxSurnameLength)
+ .Matches(SurnameRegex);
+
+ RuleFor(r => r.Username)
+ .NotEmpty()
+ .MaximumLength(MaxUsernameLength)
+ .Matches(UsernameRegex);
+
+ RuleFor(r => r.Email)
+ .NotEmpty()
+ .MaximumLength(MaxEmailLength)
+ .EmailAddress();
+
+ RuleFor(r => r.Password)
+ .NotEmpty()
+ .Matches(PasswordRegex)
+ .WithMessage(
+ "Password must contain at least one uppercase letter, " +
+ "one lowercase letter, one number, and be at least 6 characters long.");
+
+ RuleFor(r => r.Phone)
+ .Matches(PhoneRegex)
+ .When(r => !string.IsNullOrEmpty(r.Phone));
+
+ RuleFor(r => r.Address)
+ .NotNull()
+ .SetValidator(new AddressValidator()!);
+
+ RuleFor(r => r.Role)
+ .NotEmpty()
+ .Must(r => r == UserRole.User.ToString() || r == UserRole.Organizer.ToString())
+ .WithMessage($"Role must be either '{UserRole.User.ToString()}' or '{UserRole.Organizer.ToString()}'.");
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/CommentValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/CommentValidator.cs
new file mode 100644
index 00000000..9c2ce1aa
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/CommentValidator.cs
@@ -0,0 +1,16 @@
+using ReasnAPI.Models.DTOs;
+using FluentValidation;
+
+namespace ReasnAPI.Validators;
+
+public class CommentValidator : AbstractValidator
+{
+ private const int MaxContentLength = 1024;
+
+ public CommentValidator()
+ {
+ RuleFor(c => c.Content)
+ .NotEmpty()
+ .MaximumLength(MaxContentLength);
+ }
+}
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/EventValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/EventValidator.cs
new file mode 100644
index 00000000..bc4d4d51
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/EventValidator.cs
@@ -0,0 +1,41 @@
+using ReasnAPI.Models.DTOs;
+using FluentValidation;
+
+namespace ReasnAPI.Validators;
+
+public class EventValidator : AbstractValidator
+{
+ private const int MaxNameLength = 64;
+ private const int MaxDescriptionLength = 4048;
+ private const int MaxSlugLength = 128;
+
+ private const string SlugRegex = @"^[\p{L}\d]+[\p{L}\d-]*$";
+
+ public EventValidator()
+ {
+ RuleFor(e => e.Name)
+ .NotEmpty()
+ .MaximumLength(MaxNameLength);
+
+ RuleFor(e => e.Description)
+ .NotEmpty()
+ .MaximumLength(MaxDescriptionLength);
+
+ RuleFor(e => e.StartAt)
+ .LessThan(e => e.EndAt)
+ .WithMessage("'StartAt' must be before 'EndAt'.");
+
+ RuleFor(e => e.Slug)
+ .NotEmpty()
+ .MaximumLength(MaxSlugLength)
+ .Matches(SlugRegex);
+
+ RuleForEach(e => e.Tags)
+ .SetValidator(new TagValidator())
+ .When(e => e.Tags?.Count > 0);
+
+ RuleForEach(e => e.Parameters)
+ .SetValidator(new ParameterValidator())
+ .When(e => e.Parameters?.Count > 0);
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/InterestValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/InterestValidator.cs
new file mode 100644
index 00000000..2b485429
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/InterestValidator.cs
@@ -0,0 +1,19 @@
+using ReasnAPI.Models.DTOs;
+using FluentValidation;
+
+namespace ReasnAPI.Validators;
+
+public class InterestValidator : AbstractValidator
+{
+ private const int MaxNameLength = 32;
+
+ private const string NameRegex = @"^\p{Lu}\p{Ll}+(?:\s\p{L}+)*$";
+
+ public InterestValidator()
+ {
+ RuleFor(i => i.Name)
+ .NotEmpty()
+ .MaximumLength(MaxNameLength)
+ .Matches(NameRegex);
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/ParameterValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/ParameterValidator.cs
new file mode 100644
index 00000000..54894136
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/ParameterValidator.cs
@@ -0,0 +1,26 @@
+using ReasnAPI.Models.DTOs;
+using FluentValidation;
+
+namespace ReasnAPI.Validators;
+
+public class ParameterValidator : AbstractValidator
+{
+ private const int MaxKeyLength = 32;
+ private const int MaxValueLength = 64;
+
+ private const string KeyRegex = @"^\p{L}+(?:\s\p{L}+)*$";
+ private const string ValueRegex = @"^[\p{L}\d]+(?:\s[\p{L}\d]+)*$";
+
+ public ParameterValidator()
+ {
+ RuleFor(p => p.Key)
+ .NotEmpty()
+ .MaximumLength(MaxKeyLength)
+ .Matches(KeyRegex);
+
+ RuleFor(p => p.Value)
+ .NotEmpty()
+ .MaximumLength(MaxValueLength)
+ .Matches(ValueRegex);
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/TagValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/TagValidator.cs
new file mode 100644
index 00000000..641ab05a
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/TagValidator.cs
@@ -0,0 +1,19 @@
+using ReasnAPI.Models.DTOs;
+using FluentValidation;
+
+namespace ReasnAPI.Validators;
+
+public class TagValidator : AbstractValidator
+{
+ private const int MaxNameLength = 64;
+
+ private const string NameRegex = @"^\p{L}+(?:\s\p{L}+)*$";
+
+ public TagValidator()
+ {
+ RuleFor(t => t.Name)
+ .NotEmpty()
+ .MaximumLength(MaxNameLength)
+ .Matches(NameRegex);
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/Validators/UserValidator.cs b/Server/ReasnAPI/ReasnAPI/Validators/UserValidator.cs
new file mode 100644
index 00000000..fa072029
--- /dev/null
+++ b/Server/ReasnAPI/ReasnAPI/Validators/UserValidator.cs
@@ -0,0 +1,44 @@
+using ReasnAPI.Models.DTOs;
+using FluentValidation;
+
+namespace ReasnAPI.Validators;
+
+public class UserValidator : AbstractValidator
+{
+ private const int MaxNameLength = 64;
+ private const int MaxSurnameLength = 64;
+ private const int MaxUsernameLength = 64;
+ private const int MaxEmailLength = 255;
+
+ private const string NameRegex = @"^\p{Lu}[\p{Ll}\s'-]+$";
+ private const string SurnameRegex = @"^\p{L}+(?:[\s'-]\p{L}+)*$";
+ private const string UsernameRegex = @"^[\p{L}\d._%+-]{4,}$";
+ private const string PhoneRegex = @"^\+\d{1,3}\s\d{1,15}$";
+
+ public UserValidator()
+ {
+ RuleFor(r => r.Name)
+ .NotEmpty()
+ .MaximumLength(MaxNameLength)
+ .Matches(NameRegex);
+
+ RuleFor(r => r.Surname)
+ .NotEmpty()
+ .MaximumLength(MaxSurnameLength)
+ .Matches(SurnameRegex);
+
+ RuleFor(r => r.Username)
+ .NotEmpty()
+ .MaximumLength(MaxUsernameLength)
+ .Matches(UsernameRegex);
+
+ RuleFor(r => r.Email)
+ .NotEmpty()
+ .MaximumLength(MaxEmailLength)
+ .EmailAddress();
+
+ RuleFor(r => r.Phone)
+ .Matches(PhoneRegex)
+ .When(r => !string.IsNullOrEmpty(r.Phone));
+ }
+}
\ No newline at end of file
diff --git a/Server/ReasnAPI/ReasnAPI/appsettings.json b/Server/ReasnAPI/ReasnAPI/appsettings.json
index 826f8080..58588f6e 100644
--- a/Server/ReasnAPI/ReasnAPI/appsettings.json
+++ b/Server/ReasnAPI/ReasnAPI/appsettings.json
@@ -2,21 +2,21 @@
"Serilog": {
"Using": [
"Serilog.Sinks.Console"
- //"Serilog.Sinks.File"
],
"WriteTo": [
{ "Name": "Console" }
- //{
- // "Name": "File",
- // "Args": {
- // "path": "logs/ReasnAPI-.log",
- // "rollingInterval": "Day"
- // }
- //}
]
},
"ConnectionStrings": {
"DefaultValue": "Server=postgres;Port=5432;Database=reasn;User Id=dba;Password=sql;"
},
+ "JwtSettings": {
+ "Issuer": "http://localhost:5272",
+ "Audiences": [
+ "http://localhost:5272"
+ ],
+ "Key": "YOUR_SECRET_KEY",
+ "DurationInHours": 8
+ },
"AllowedHosts": "*"
}
diff --git a/compose.override.yml b/compose.override.yml
index 78ce04ea..17c93175 100644
--- a/compose.override.yml
+++ b/compose.override.yml
@@ -47,6 +47,8 @@ services:
path: ./Server/ReasnAPI/ReasnAPI
postgres:
+ ports:
+ - "5432:5432"
volumes:
- ./Database/init-dev-data.sql:/docker-entrypoint-initdb.d/03-init-dev-data.sql
-
\ No newline at end of file
+