0
0
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:
Alexander Konietzko 2023-03-17 08:44:37 +01:00
parent 74d546f6c7
commit b36aaff112
No known key found for this signature in database
GPG Key ID: BA6905F37AEC2B5B
18 changed files with 221 additions and 19 deletions

View File

@ -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" />

View File

@ -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)
{

View File

@ -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 { }

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -3,4 +3,5 @@ namespace CleanArchitecture.Application.ViewModels.Users;
public sealed record CreateUserViewModel(
string Email,
string Surname,
string GivenName);
string GivenName,
string Password);

View File

@ -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
};
}
}

View 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;
}

View File

@ -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>

View File

@ -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()

View File

@ -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);

View File

@ -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())

View File

@ -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()

View File

@ -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())

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
namespace CleanArchitecture.Domain.Enums;
public enum UserRole
{
Admin,
User
}

View File

@ -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;
}
}

View 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; }
}