diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj
index 66b0cf6..7b9dbb2 100644
--- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj
+++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj
@@ -3,9 +3,11 @@
net7.0
enable
+ 64377c40-44d6-4989-9662-5d778f8b3b92
+
diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs
index 5d6cfc6..ff9aa64 100644
--- a/CleanArchitecture.Api/Controllers/UserController.cs
+++ b/CleanArchitecture.Api/Controllers/UserController.cs
@@ -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 GetAllUsersAsync()
{
var users = await _userService.GetAllUsersAsync();
return Response(users);
}
-
+
+ [Authorize]
[HttpGet("{id}")]
public async Task 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 DeleteUserAsync([FromRoute] Guid id)
{
await _userService.DeleteUserAsync(id);
return Response(id);
}
-
+
+ [Authorize]
[HttpPut]
public async Task UpdateUserAsync([FromBody] UpdateUserViewModel viewModel)
{
diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs
index 4aee2bc..87dc4ee 100644
--- a/CleanArchitecture.Api/Program.cs
+++ b/CleanArchitecture.Api/Program.cs
@@ -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(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 { }
\ No newline at end of file
diff --git a/CleanArchitecture.Api/appsettings.Development.json b/CleanArchitecture.Api/appsettings.Development.json
index 0c208ae..c183fbb 100644
--- a/CleanArchitecture.Api/appsettings.Development.json
+++ b/CleanArchitecture.Api/appsettings.Development.json
@@ -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"
}
}
diff --git a/CleanArchitecture.Api/appsettings.json b/CleanArchitecture.Api/appsettings.json
index 26b0156..c481317 100644
--- a/CleanArchitecture.Api/appsettings.json
+++ b/CleanArchitecture.Api/appsettings.json
@@ -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"
}
}
diff --git a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs
index c1a01d4..3efbb36 100644
--- a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs
+++ b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs
@@ -3,4 +3,5 @@ namespace CleanArchitecture.Application.ViewModels.Users;
public sealed record CreateUserViewModel(
string Email,
string Surname,
- string GivenName);
\ No newline at end of file
+ string GivenName,
+ string Password);
\ No newline at end of file
diff --git a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs
index 9ecc82f..cc0ac67 100644
--- a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs
+++ b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs
@@ -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
};
}
}
diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs
new file mode 100644
index 0000000..705bd4e
--- /dev/null
+++ b/CleanArchitecture.Domain/ApiUser.cs
@@ -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;
+}
diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
index 8c0626a..496b495 100644
--- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
+++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
@@ -6,8 +6,10 @@
+
+
diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
index 6106289..7185a16 100644
--- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
@@ -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()
diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
index 0ebbd00..234364c 100644
--- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
@@ -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);
diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs
index edf9b0c..e912010 100644
--- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs
@@ -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
{
private readonly IUserRepository _userRepository;
-
+ private readonly IUser _user;
+
public DeleteUserCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler 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())
diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
index 44e3617..d561e47 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
@@ -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()
diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
index a94c5b5..99c6820 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
@@ -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
{
private readonly IUserRepository _userRepository;
-
+ private readonly IUser _user;
+
public UpdateUserCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler 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())
diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs
index e2c56e9..2d14858 100644
--- a/CleanArchitecture.Domain/Entities/User.cs
+++ b/CleanArchitecture.Domain/Entities/User.cs
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Enums/UserRole.cs b/CleanArchitecture.Domain/Enums/UserRole.cs
new file mode 100644
index 0000000..95c9259
--- /dev/null
+++ b/CleanArchitecture.Domain/Enums/UserRole.cs
@@ -0,0 +1,7 @@
+namespace CleanArchitecture.Domain.Enums;
+
+public enum UserRole
+{
+ Admin,
+ User
+}
diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs
index 64cd05d..f827036 100644
--- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs
+++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs
@@ -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();
+
+ return services;
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Interfaces/IUser.cs b/CleanArchitecture.Domain/Interfaces/IUser.cs
new file mode 100644
index 0000000..55e953c
--- /dev/null
+++ b/CleanArchitecture.Domain/Interfaces/IUser.cs
@@ -0,0 +1,12 @@
+using System;
+using CleanArchitecture.Domain.Enums;
+
+namespace CleanArchitecture.Domain.Interfaces;
+
+public interface IUser
+{
+ Guid GetUserId();
+ UserRole GetUserRole();
+
+ string Name { get; }
+}