mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-08-23 03:38:36 +00:00
Add User auth
This commit is contained in:
parent
74d546f6c7
commit
b36aaff112
@ -3,9 +3,11 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>64377c40-44d6-4989-9662-5d778f8b3b92</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
|
@ -4,6 +4,7 @@ using CleanArchitecture.Application.Interfaces;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using CleanArchitecture.Domain.Notifications;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CleanArchitecture.Api.Controllers;
|
||||
@ -21,13 +22,15 @@ public class UserController : ApiController
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAllUsersAsync()
|
||||
{
|
||||
var users = await _userService.GetAllUsersAsync();
|
||||
return Response(users);
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> GetUserByIdAsync(
|
||||
[FromRoute] Guid id,
|
||||
@ -43,14 +46,16 @@ public class UserController : ApiController
|
||||
var userId = await _userService.CreateUserAsync(viewModel);
|
||||
return Response(userId);
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid id)
|
||||
{
|
||||
await _userService.DeleteUserAsync(id);
|
||||
return Response(id);
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> UpdateUserAsync([FromBody] UpdateUserViewModel viewModel)
|
||||
{
|
||||
|
@ -1,12 +1,16 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Text;
|
||||
using CleanArchitecture.Application.Extensions;
|
||||
using CleanArchitecture.Domain.Extensions;
|
||||
using CleanArchitecture.gRPC;
|
||||
using CleanArchitecture.Infrastructure.Database;
|
||||
using CleanArchitecture.Infrastructure.Extensions;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@ -24,11 +28,23 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
b => b.MigrationsAssembly("CleanArchitecture.Infrastructure"));
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication(
|
||||
options =>
|
||||
{
|
||||
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(
|
||||
jwtOptions =>
|
||||
{
|
||||
jwtOptions.TokenValidationParameters = CreateTokenValidationParameters();
|
||||
});
|
||||
|
||||
builder.Services.AddInfrastructure();
|
||||
builder.Services.AddQueryHandlers();
|
||||
builder.Services.AddServices();
|
||||
builder.Services.AddCommandHandlers();
|
||||
builder.Services.AddNotificationHandlers();
|
||||
builder.Services.AddApiUser();
|
||||
|
||||
builder.Services.AddMediatR(cfg =>
|
||||
{
|
||||
@ -42,6 +58,7 @@ app.UseSwaggerUI();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
@ -57,5 +74,24 @@ using (IServiceScope scope = app.Services.CreateScope())
|
||||
|
||||
app.Run();
|
||||
|
||||
TokenValidationParameters CreateTokenValidationParameters()
|
||||
{
|
||||
var result = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = builder.Configuration["Auth:Issuer"],
|
||||
ValidAudience = builder.Configuration["Auth:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(
|
||||
builder.Configuration["Auth:Secret"]!)),
|
||||
RequireSignedTokens = false
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Needed for integration tests webapplication factory
|
||||
public partial class Program { }
|
@ -4,5 +4,13 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
|
||||
},
|
||||
"Auth": {
|
||||
"Issuer": "CleanArchitectureServer",
|
||||
"Audience": "CleanArchitectureClient",
|
||||
"Secret": "sD3v061gf8BxXgmxcHss"
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,10 @@
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
|
||||
},
|
||||
"Auth": {
|
||||
"Issuer": "CleanArchitectureServer",
|
||||
"Audience": "CleanArchitectureClient",
|
||||
"Secret": "sD3v061gf8BxXgmxcHss"
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,5 @@ namespace CleanArchitecture.Application.ViewModels.Users;
|
||||
public sealed record CreateUserViewModel(
|
||||
string Email,
|
||||
string Surname,
|
||||
string GivenName);
|
||||
string GivenName,
|
||||
string Password);
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
|
||||
namespace CleanArchitecture.Application.ViewModels.Users;
|
||||
|
||||
@ -9,6 +10,7 @@ public sealed class UserViewModel
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string GivenName { get; set; } = string.Empty;
|
||||
public string Surname { get; set; } = string.Empty;
|
||||
public UserRole Role { get; set; }
|
||||
|
||||
public static UserViewModel FromUser(User user)
|
||||
{
|
||||
@ -17,7 +19,8 @@ public sealed class UserViewModel
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
GivenName = user.GivenName,
|
||||
Surname = user.Surname
|
||||
Surname = user.Surname,
|
||||
Role = user.Role
|
||||
};
|
||||
}
|
||||
}
|
||||
|
46
CleanArchitecture.Domain/ApiUser.cs
Normal file
46
CleanArchitecture.Domain/ApiUser.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace CleanArchitecture.Domain;
|
||||
|
||||
public sealed class ApiUser : IUser
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public ApiUser(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public Guid GetUserId()
|
||||
{
|
||||
var claim = _httpContextAccessor.HttpContext?.User.Claims
|
||||
.FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier));
|
||||
|
||||
if (Guid.TryParse(claim?.Value, out var userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Could not parse user id to guid");
|
||||
}
|
||||
|
||||
public UserRole GetUserRole()
|
||||
{
|
||||
var claim = _httpContextAccessor.HttpContext?.User.Claims
|
||||
.FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Role));
|
||||
|
||||
if (Enum.TryParse(claim?.Value, out UserRole userRole))
|
||||
{
|
||||
return userRole;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Could not parse user role");
|
||||
}
|
||||
|
||||
public string Name => _httpContextAccessor.HttpContext?.User.Identity?.Name ?? string.Empty;
|
||||
}
|
@ -6,8 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="FluentValidation" Version="11.5.1" />
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -10,17 +10,20 @@ public sealed class CreateUserCommand : CommandBase
|
||||
public string Email { get; }
|
||||
public string Surname { get; }
|
||||
public string GivenName { get; }
|
||||
|
||||
public string Password { get; }
|
||||
|
||||
public CreateUserCommand(
|
||||
Guid userId,
|
||||
string email,
|
||||
string surname,
|
||||
string givenName) : base(userId)
|
||||
string givenName,
|
||||
string password) : base(userId)
|
||||
{
|
||||
UserId = userId;
|
||||
Email = email;
|
||||
Surname = surname;
|
||||
GivenName = givenName;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
public override bool IsValid()
|
||||
|
@ -1,12 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.User;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using CleanArchitecture.Domain.Notifications;
|
||||
using MediatR;
|
||||
using BC = BCrypt.Net.BCrypt;
|
||||
|
||||
namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
|
||||
|
||||
@ -43,11 +45,15 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
|
||||
return;
|
||||
}
|
||||
|
||||
var passwordHash = BC.HashPassword(request.Password);
|
||||
|
||||
var user = new User(
|
||||
request.UserId,
|
||||
request.Email,
|
||||
request.Surname,
|
||||
request.GivenName);
|
||||
request.GivenName,
|
||||
passwordHash,
|
||||
UserRole.User);
|
||||
|
||||
_userRepository.Add(user);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.User;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
@ -13,14 +14,17 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
|
||||
IRequestHandler<DeleteUserCommand>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
private readonly IUser _user;
|
||||
|
||||
public DeleteUserCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> notifications,
|
||||
IUserRepository userRepository) : base(bus, unitOfWork, notifications)
|
||||
IUserRepository userRepository,
|
||||
IUser user) : base(bus, unitOfWork, notifications)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_user = user;
|
||||
}
|
||||
|
||||
public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken)
|
||||
@ -43,6 +47,11 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
|
||||
return;
|
||||
}
|
||||
|
||||
if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_userRepository.Remove(user);
|
||||
|
||||
if (await CommitAsync())
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
|
||||
namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
||||
|
||||
@ -10,17 +11,20 @@ public sealed class UpdateUserCommand : CommandBase
|
||||
public string Email { get; }
|
||||
public string Surname { get; }
|
||||
public string GivenName { get; }
|
||||
|
||||
public UserRole Role { get; }
|
||||
|
||||
public UpdateUserCommand(
|
||||
Guid userId,
|
||||
string email,
|
||||
string surname,
|
||||
string givenName) : base(userId)
|
||||
string givenName,
|
||||
UserRole role) : base(userId)
|
||||
{
|
||||
UserId = userId;
|
||||
Email = email;
|
||||
Surname = surname;
|
||||
GivenName = givenName;
|
||||
Role = role;
|
||||
}
|
||||
|
||||
public override bool IsValid()
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Errors;
|
||||
using CleanArchitecture.Domain.Events.User;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
@ -13,14 +15,17 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
||||
IRequestHandler<UpdateUserCommand>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
private readonly IUser _user;
|
||||
|
||||
public UpdateUserCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> notifications,
|
||||
IUserRepository userRepository) : base(bus, unitOfWork, notifications)
|
||||
IUserRepository userRepository,
|
||||
IUser user) : base(bus, unitOfWork, notifications)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_user = user;
|
||||
}
|
||||
|
||||
public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken)
|
||||
@ -41,11 +46,21 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
|
||||
ErrorCodes.ObjectNotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_user.GetUserRole() == UserRole.Admin)
|
||||
{
|
||||
user.SetRole(request.Role);
|
||||
}
|
||||
|
||||
user.SetEmail(request.Email);
|
||||
user.SetSurname(request.Surname);
|
||||
user.SetGivenName(request.GivenName);
|
||||
|
||||
|
||||
_userRepository.Update(user);
|
||||
|
||||
if (await CommitAsync())
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
|
||||
namespace CleanArchitecture.Domain.Entities;
|
||||
|
||||
@ -8,20 +9,26 @@ public class User : Entity
|
||||
public string Email { get; private set; }
|
||||
public string GivenName { get; private set; }
|
||||
public string Surname { get; private set; }
|
||||
public string Password { get; private set; }
|
||||
public UserRole Role { get; private set; }
|
||||
|
||||
public string FullName => $"{Surname}, {GivenName}";
|
||||
|
||||
|
||||
public User(
|
||||
Guid id,
|
||||
string email,
|
||||
string surname,
|
||||
string givenName) : base(id)
|
||||
string givenName,
|
||||
string password,
|
||||
UserRole role) : base(id)
|
||||
{
|
||||
Email = email;
|
||||
GivenName = givenName;
|
||||
Surname = surname;
|
||||
Password = password;
|
||||
Role = role;
|
||||
}
|
||||
|
||||
|
||||
[MemberNotNull(nameof(Email))]
|
||||
public void SetEmail(string email)
|
||||
{
|
||||
@ -72,4 +79,26 @@ public class User : Entity
|
||||
|
||||
Surname = surname;
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(Password))]
|
||||
public void SetPassword(string password)
|
||||
{
|
||||
if (password == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
}
|
||||
|
||||
if (password.Length > 100)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Password may not be longer than 100 characters");
|
||||
}
|
||||
|
||||
Password = password;
|
||||
}
|
||||
|
||||
public void SetRole(UserRole role)
|
||||
{
|
||||
Role = role;
|
||||
}
|
||||
}
|
7
CleanArchitecture.Domain/Enums/UserRole.cs
Normal file
7
CleanArchitecture.Domain/Enums/UserRole.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace CleanArchitecture.Domain.Enums;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
Admin,
|
||||
User
|
||||
}
|
@ -3,6 +3,7 @@ using CleanArchitecture.Domain.Commands.Users.DeleteUser;
|
||||
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
|
||||
using CleanArchitecture.Domain.EventHandler;
|
||||
using CleanArchitecture.Domain.Events.User;
|
||||
using CleanArchitecture.Domain.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -29,4 +30,12 @@ public static class ServiceCollectionExtension
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddApiUser(this IServiceCollection services)
|
||||
{
|
||||
// User
|
||||
services.AddScoped<IUser, ApiUser>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
12
CleanArchitecture.Domain/Interfaces/IUser.cs
Normal file
12
CleanArchitecture.Domain/Interfaces/IUser.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
|
||||
namespace CleanArchitecture.Domain.Interfaces;
|
||||
|
||||
public interface IUser
|
||||
{
|
||||
Guid GetUserId();
|
||||
UserRole GetUserRole();
|
||||
|
||||
string Name { get; }
|
||||
}
|
Loading…
Reference in New Issue
Block a user