diff --git a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
index 3879c49..618d4dc 100644
--- a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
+++ b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj
@@ -7,11 +7,11 @@
-
+
-
-
-
+
+
+
diff --git a/src/CMSMicroservice.Application/Common/Exceptions/ValidationException.cs b/src/CMSMicroservice.Application/Common/Exceptions/ValidationException.cs
index 77d2d7d..bb3bece 100644
--- a/src/CMSMicroservice.Application/Common/Exceptions/ValidationException.cs
+++ b/src/CMSMicroservice.Application/Common/Exceptions/ValidationException.cs
@@ -11,12 +11,30 @@ public class ValidationException : Exception
}
public ValidationException(IEnumerable failures)
- : this()
+ : base(BuildMessage(failures, out var errors))
{
- Errors = failures
- .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
- .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
+ Errors = errors;
}
public IDictionary Errors { get; }
+
+ private static string BuildMessage(IEnumerable failures, out IDictionary errors)
+ {
+ var list = failures?.Where(f => !string.IsNullOrWhiteSpace(f.ErrorMessage)).ToList() ?? new List();
+
+ errors = list
+ .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
+ .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
+
+ var items = list
+ .Select(f => string.IsNullOrWhiteSpace(f.PropertyName)
+ ? f.ErrorMessage
+ : $"{f.PropertyName}: {f.ErrorMessage}")
+ .Where(s => !string.IsNullOrWhiteSpace(s))
+ .ToArray();
+
+ return items.Length > 0
+ ? string.Join(" | ", items)
+ : "One or more validation failures have occurred.";
+ }
}
diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IGenerateJwtToken.cs b/src/CMSMicroservice.Application/Common/Interfaces/IGenerateJwtToken.cs
index 1b08985..2a53ce8 100644
--- a/src/CMSMicroservice.Application/Common/Interfaces/IGenerateJwtToken.cs
+++ b/src/CMSMicroservice.Application/Common/Interfaces/IGenerateJwtToken.cs
@@ -1,5 +1,5 @@
namespace CMSMicroservice.Application.Common.Interfaces;
public interface IGenerateJwtToken
{
- Task GenerateJwtToken(User user);
+ Task GenerateJwtToken(User user,int? expiryDays=null);
}
diff --git a/src/CMSMicroservice.Application/UserCQ/Commands/SetPasswordForUser/SetPasswordForUserCommandHandler.cs b/src/CMSMicroservice.Application/UserCQ/Commands/SetPasswordForUser/SetPasswordForUserCommandHandler.cs
index 72ca63e..60cfa1a 100644
--- a/src/CMSMicroservice.Application/UserCQ/Commands/SetPasswordForUser/SetPasswordForUserCommandHandler.cs
+++ b/src/CMSMicroservice.Application/UserCQ/Commands/SetPasswordForUser/SetPasswordForUserCommandHandler.cs
@@ -3,15 +3,50 @@ namespace CMSMicroservice.Application.UserCQ.Commands.SetPasswordForUser;
public class SetPasswordForUserCommandHandler : IRequestHandler
{
private readonly IApplicationDbContext _context;
+ private readonly IHashService _hashService;
- public SetPasswordForUserCommandHandler(IApplicationDbContext context)
+ public SetPasswordForUserCommandHandler(IApplicationDbContext context, IHashService hashService)
{
_context = context;
+ _hashService = hashService;
}
public async Task Handle(SetPasswordForUserCommand request, CancellationToken cancellationToken)
{
- //TODO: Implement your business logic
- return new Unit();
+ // basic validations
+ if (!string.Equals(request.NewPassword, request.ConfirmPassword, StringComparison.Ordinal))
+ {
+ throw new CMSMicroservice.Application.Common.Exceptions.ValidationException(new[]
+ {
+ new FluentValidation.Results.ValidationFailure(nameof(request.ConfirmPassword), "کلمه عبور و تایید آن یکسان نیستند.")
+ });
+ }
+
+ var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken)
+ ?? throw new NotFoundException(nameof(User), request.UserId);
+
+ var hasExistingPassword = !string.IsNullOrWhiteSpace(user.HashPassword);
+ if (hasExistingPassword)
+ {
+ if (string.IsNullOrWhiteSpace(request.CurrentPassword))
+ {
+ throw new CMSMicroservice.Application.Common.Exceptions.ValidationException(new[]
+ {
+ new FluentValidation.Results.ValidationFailure(nameof(request.CurrentPassword), "کلمه عبور فعلی الزامی است.")
+ });
+ }
+ if (!_hashService.VerifyPassword(request.CurrentPassword, user.HashPassword))
+ {
+ throw new UnauthorizedAccessException("کلمه عبور فعلی نادرست است.");
+ }
+ }
+
+ // set new password (PBKDF2)
+ user.HashPassword = _hashService.HashPassword(request.NewPassword);
+ _context.Users.Update(user);
+ user.AddDomainEvent(new CMSMicroservice.Domain.Events.SetPasswordForUserEvent(user));
+ await _context.SaveChangesAsync(cancellationToken);
+
+ return Unit.Value;
}
}
diff --git a/src/CMSMicroservice.Application/UserCQ/EventHandlers/SetPasswordForUserEventHandlers/SetPasswordForUserEventHandler.cs b/src/CMSMicroservice.Application/UserCQ/EventHandlers/SetPasswordForUserEventHandlers/SetPasswordForUserEventHandler.cs
index 062d465..d52e925 100644
--- a/src/CMSMicroservice.Application/UserCQ/EventHandlers/SetPasswordForUserEventHandlers/SetPasswordForUserEventHandler.cs
+++ b/src/CMSMicroservice.Application/UserCQ/EventHandlers/SetPasswordForUserEventHandlers/SetPasswordForUserEventHandler.cs
@@ -1,7 +1,7 @@
using CMSMicroservice.Domain.Events;
using Microsoft.Extensions.Logging;
-namespace CMSMicroservice.Application.UserCQ.EventHandlers;
+namespace CMSMicroservice.Application.UserCQ.EventHandlers.SetPasswordForUserEventHandlers;
public class SetPasswordForUserEventHandler : INotificationHandler
{
diff --git a/src/CMSMicroservice.Application/UserCQ/Queries/AdminGetJwtToken/AdminGetJwtTokenQueryHandler.cs b/src/CMSMicroservice.Application/UserCQ/Queries/AdminGetJwtToken/AdminGetJwtTokenQueryHandler.cs
index b93985a..f2866e3 100644
--- a/src/CMSMicroservice.Application/UserCQ/Queries/AdminGetJwtToken/AdminGetJwtTokenQueryHandler.cs
+++ b/src/CMSMicroservice.Application/UserCQ/Queries/AdminGetJwtToken/AdminGetJwtTokenQueryHandler.cs
@@ -1,18 +1,21 @@
namespace CMSMicroservice.Application.UserCQ.Queries.AdminGetJwtToken;
+
public class AdminGetJwtTokenQueryHandler : IRequestHandler
{
private readonly IApplicationDbContext _context;
private readonly IGenerateJwtToken _generateJwt;
private readonly IHashService _hashService;
- public AdminGetJwtTokenQueryHandler(IApplicationDbContext context, IGenerateJwtToken generateJwt, IHashService hashService)
+ public AdminGetJwtTokenQueryHandler(IApplicationDbContext context, IGenerateJwtToken generateJwt,
+ IHashService hashService)
{
_context = context;
_generateJwt = generateJwt;
_hashService = hashService;
}
- public async Task Handle(AdminGetJwtTokenQuery request, CancellationToken cancellationToken)
+ public async Task Handle(AdminGetJwtTokenQuery request,
+ CancellationToken cancellationToken)
{
var user = await _context.Users
.Include(u => u.UserRoles)
@@ -21,14 +24,16 @@ public class AdminGetJwtTokenQueryHandler : IRequestHandler a.RoleId != 2))
+ throw new Exception("You do not have permission to do that.");
+
// verify password (supports PBKDF2 or legacy SHA-256)
if (!_hashService.VerifyPassword(request.Password, user.HashPassword))
throw new Exception("Invalid username or password.");
return new AdminGetJwtTokenResponseDto()
{
- Token = await _generateJwt.GenerateJwtToken(user),
+ Token = await _generateJwt.GenerateJwtToken(user,15),
};
-
}
-}
+}
\ No newline at end of file
diff --git a/src/CMSMicroservice.Domain/Events/UserEvents/SetPasswordForUserEvent.cs b/src/CMSMicroservice.Domain/Events/UserEvents/SetPasswordForUserEvent.cs
index ea8c7da..8832386 100644
--- a/src/CMSMicroservice.Domain/Events/UserEvents/SetPasswordForUserEvent.cs
+++ b/src/CMSMicroservice.Domain/Events/UserEvents/SetPasswordForUserEvent.cs
@@ -3,6 +3,7 @@ public class SetPasswordForUserEvent : BaseEvent
{
public SetPasswordForUserEvent(User item)
{
+ Item = item;
}
public User Item { get; }
}
diff --git a/src/CMSMicroservice.Infrastructure/CMSMicroservice.Infrastructure.csproj b/src/CMSMicroservice.Infrastructure/CMSMicroservice.Infrastructure.csproj
index 6b0d4a6..5261df4 100644
--- a/src/CMSMicroservice.Infrastructure/CMSMicroservice.Infrastructure.csproj
+++ b/src/CMSMicroservice.Infrastructure/CMSMicroservice.Infrastructure.csproj
@@ -6,15 +6,15 @@
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs
index 79c84d9..f84e914 100644
--- a/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/src/CMSMicroservice.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -18,7 +18,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("CMS")
- .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
@@ -474,6 +474,9 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property("FirstName")
.HasColumnType("nvarchar(max)");
+ b.Property("HashPassword")
+ .HasColumnType("nvarchar(max)");
+
b.Property("IsDeleted")
.HasColumnType("bit");
diff --git a/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs b/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs
index 381e15e..ae8bc27 100644
--- a/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs
+++ b/src/CMSMicroservice.Infrastructure/Services/GenerateJwtTokenService.cs
@@ -17,7 +17,7 @@ public class GenerateJwtTokenService : IGenerateJwtToken
_configuration = configuration;
}
- public async Task GenerateJwtToken(User user)
+ public async Task GenerateJwtToken(User user,int? expiryDays=null)
{
var lastModified = user.LastModified ?? user.Created;
var claims = new List
@@ -51,7 +51,10 @@ public class GenerateJwtTokenService : IGenerateJwtToken
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
- var expiry = DateTime.Now.AddDays(Convert.ToInt32(_configuration["JwtExpiryInDays"]));
+ var expiry =expiryDays!=null?
+ DateTime.Now.AddDays(Convert.ToInt32(expiryDays))
+ :
+ DateTime.Now.AddDays(Convert.ToInt32(_configuration["JwtExpiryInDays"]));
var token = new JwtSecurityToken(
_configuration["JwtIssuer"],
diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj
index 7d9057f..7598d70 100644
--- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj
+++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj
@@ -3,7 +3,7 @@
net9.0
enable
enable
- 0.0.117
+ 0.0.118
None
False
False
diff --git a/src/CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj b/src/CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj
index ba6ad0e..ae90b87 100644
--- a/src/CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj
+++ b/src/CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj
@@ -15,16 +15,16 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
-
+
+
+
diff --git a/src/CMSMicroservice.WebApi/Common/Behaviours/ExceptionHandlingBehaviour.cs b/src/CMSMicroservice.WebApi/Common/Behaviours/ExceptionHandlingBehaviour.cs
new file mode 100644
index 0000000..74ca6cf
--- /dev/null
+++ b/src/CMSMicroservice.WebApi/Common/Behaviours/ExceptionHandlingBehaviour.cs
@@ -0,0 +1,99 @@
+// filepath: /media/masoud/Project/FourSat/CMS/src/CMSMicroservice.WebApi/Common/Behaviours/ExceptionHandlingBehaviour.cs
+using System.Text.Json;
+using System.Collections.Generic;
+using CMSMicroservice.Application.Common.Exceptions;
+using Grpc.Core;
+using Grpc.Core.Interceptors;
+using Microsoft.Extensions.Logging;
+
+namespace CMSMicroservice.WebApi.Common.Behaviours;
+
+public class ExceptionHandlingBehaviour : Interceptor
+{
+ private readonly ILogger _logger;
+
+ public ExceptionHandlingBehaviour(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public override async Task UnaryServerHandler(
+ TRequest request,
+ ServerCallContext context,
+ UnaryServerMethod continuation)
+ {
+ try
+ {
+ return await continuation(request, context);
+ }
+ catch (ValidationException vex)
+ {
+ // Flatten validation errors into a trailer so clients can display them
+ var metadata = new Metadata();
+ string description = vex.Message;
+ try
+ {
+ if (vex.Errors is { Count: > 0 })
+ {
+ var payload = JsonSerializer.Serialize(vex.Errors);
+ metadata.Add("validation-errors-text", payload);
+
+ // Build a human-friendly description out of individual messages
+ var parts = new List();
+ foreach (var kv in vex.Errors)
+ {
+ foreach (var msg in kv.Value)
+ {
+ if (!string.IsNullOrWhiteSpace(kv.Key))
+ parts.Add($"{kv.Key}: {msg}");
+ else
+ parts.Add(msg);
+ }
+ }
+ if (parts.Count > 0)
+ {
+ description = string.Join(" | ", parts);
+ }
+ }
+ }
+ catch
+ {
+ // ignore serialization issues, still return InvalidArgument
+ }
+
+ _logger.LogWarning(vex, "Validation failed for request {Method}", context.Method);
+ throw new RpcException(new Status(StatusCode.InvalidArgument, description), metadata);
+ }
+ catch (NotFoundException nfex)
+ {
+ _logger.LogInformation(nfex, "Entity not found for request {Method}", context.Method);
+ throw new RpcException(new Status(StatusCode.NotFound, nfex.Message));
+ }
+ catch (UnauthorizedAccessException uaex)
+ {
+ _logger.LogInformation(uaex, "Unauthorized access for request {Method}", context.Method);
+ throw new RpcException(new Status(StatusCode.Unauthenticated, uaex.Message));
+ }
+ catch (ForbiddenAccessException fax)
+ {
+ _logger.LogInformation(fax, "Forbidden access for request {Method}", context.Method);
+ throw new RpcException(new Status(StatusCode.PermissionDenied, fax.Message));
+ }
+ catch (DuplicateException dex)
+ {
+ _logger.LogInformation(dex, "Duplicate resource for request {Method}", context.Method);
+ throw new RpcException(new Status(StatusCode.AlreadyExists, dex.Message));
+ }
+ catch (ArgumentException aex)
+ {
+ _logger.LogInformation(aex, "Invalid argument for request {Method}", context.Method);
+ throw new RpcException(new Status(StatusCode.InvalidArgument, aex.Message));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unhandled exception for request {Method}", context.Method);
+ // Hide internal details from clients
+ throw new RpcException(new Status(StatusCode.Internal, "Internal server error"));
+ }
+ }
+}
diff --git a/src/CMSMicroservice.WebApi/Program.cs b/src/CMSMicroservice.WebApi/Program.cs
index 11904d4..1525688 100644
--- a/src/CMSMicroservice.WebApi/Program.cs
+++ b/src/CMSMicroservice.WebApi/Program.cs
@@ -38,6 +38,7 @@ builder.Services.AddGrpc(options =>
{
options.Interceptors.Add();
options.Interceptors.Add();
+ //options.Interceptors.Add();
options.EnableDetailedErrors = true;
options.MaxReceiveMessageSize = 1000 * 1024 * 1024; // 1 GB
options.MaxSendMessageSize = 1000 * 1024 * 1024; // 1 GB
diff --git a/src/CMSMicroservice.WebApi/appsettings.json b/src/CMSMicroservice.WebApi/appsettings.json
index 9b32709..297d770 100644
--- a/src/CMSMicroservice.WebApi/appsettings.json
+++ b/src/CMSMicroservice.WebApi/appsettings.json
@@ -2,7 +2,7 @@
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
- "JwtExpiryInDays": 365,
+ "JwtExpiryInDays": 5,
"ConnectionStrings": {
"DefaultConnection": "Data Source=185.252.31.42,2019; Initial Catalog=Foursat;User ID=afrino;Password=87zH26nbqT%;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
"providerName": "System.Data.SqlClient"