внедрена защищенная смена пароля (бэк + фронт)

This commit is contained in:
max
2026-04-17 19:52:57 +03:00
parent d2e6eb578a
commit b346372555
6 changed files with 240 additions and 0 deletions
@@ -8,4 +8,7 @@ public static class AuthErrors
public static Error IdentityRegistrationDisabled => new("Auth.RegistrationDisabled", "Registration is disabled");
public static Error IdentityUsernameNotUnique => new("Auth.UsernameNotUnique", "Username is already taken");
public static Error UserNotFound => new("Auth.UserNotFound", "User not found");
public static Error PasswordConfirmationMismatch => new("Auth.PasswordConfirmationMismatch", "Passwords do not match");
public static Error PasswordTooShort => new("Auth.PasswordTooShort", "Password must be at least 8 characters");
public static Error OldPasswordInvalid => new("Auth.OldPasswordInvalid", "Current password is incorrect");
}
@@ -0,0 +1,48 @@
using BCrypt.Net;
using Knot.Contracts.Auth.Application.Abstractions;
using Knot.Contracts.Auth.Application.Auth.DTOs;
using Knot.Modules.Auth.Domain;
using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
namespace Knot.Modules.Auth.Application.Users.ChangePassword;
public sealed record ChangePasswordCommand(
Guid UserId,
string OldPassword,
string NewPassword,
string ConfirmPassword) : ICommand;
internal sealed class ChangePasswordCommandHandler : ICommandHandler<ChangePasswordCommand>
{
private readonly IAuthDbContext _dbContext;
public ChangePasswordCommandHandler(IAuthDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Result> Handle(ChangePasswordCommand request, CancellationToken cancellationToken)
{
if (request.NewPassword != request.ConfirmPassword)
return Result.Failure(AuthErrors.PasswordConfirmationMismatch);
if (request.NewPassword.Length < 8)
return Result.Failure(AuthErrors.PasswordTooShort);
var user = await _dbContext.Set<User>()
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user is null)
return Result.Failure(AuthErrors.UserNotFound);
if (!BCrypt.Net.BCrypt.Verify(request.OldPassword, user.PasswordHash))
return Result.Failure(AuthErrors.OldPasswordInvalid);
user.ChangePassword(BCrypt.Net.BCrypt.HashPassword(request.NewPassword));
user.SetRefreshToken(null);
await _dbContext.SaveChangesAsync(cancellationToken);
return Result.Success();
}
}
@@ -2,6 +2,7 @@ using Knot.Shared.Kernel;
using Knot.Modules.Auth.Application.Users.Login;
using Knot.Modules.Auth.Application.Users.Register;
using Knot.Modules.Auth.Application.Users.GetMe;
using Knot.Modules.Auth.Application.Users.ChangePassword;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
@@ -12,6 +13,8 @@ namespace Knot.Modules.Auth.Presentation.Endpoints;
public static class AuthEndpoints
{
public sealed record ChangePasswordRequest(string OldPassword, string NewPassword, string ConfirmPassword);
public static void MapAuthEndpoints(this WebApplication app)
{
var group = app.MapGroup("api/auth");
@@ -33,5 +36,19 @@ public static class AuthEndpoints
var result = await sender.Send(new GetMeQuery(userContext.UserId), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
}).RequireAuthorization();
group.MapPost("change-password", async ([FromBody] ChangePasswordRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
{
var result = await sender.Send(
new ChangePasswordCommand(userContext.UserId, request.OldPassword, request.NewPassword, request.ConfirmPassword),
ct);
if (result.IsSuccess) return Results.Ok();
if (result.Error.Code == "Auth.OldPasswordInvalid")
return Results.StatusCode(StatusCodes.Status403Forbidden);
return Results.BadRequest(new { error = result.Error.Code ?? result.Error.Description });
}).RequireAuthorization();
}
}