-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Development feature - User Management #19
Open
mikkelly-mi
wants to merge
1
commit into
ktutak1337:main
Choose a base branch
from
mikkelly-mi:user-management
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
src/Server/StellarChat.Server.Api/DAL/Mongo/Seeders/RolesSeeder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
ο»Ώusing StellarChat.Shared.Infrastructure.Identity; | ||
using StellarChat.Shared.Infrastructure.Identity.Seeders; | ||
|
||
namespace StellarChat.Server.Api.DAL.Mongo.Seeders; | ||
|
||
internal sealed class RolesSeeder(RoleManager<ApplicationRole> roleManager, ILogger<RolesSeeder> logger) : IRolesSeeder | ||
{ | ||
private readonly RoleManager<ApplicationRole> _roleManager = roleManager; | ||
private readonly ILogger<RolesSeeder> _logger = logger; | ||
|
||
public async Task SeedAsync() | ||
{ | ||
var role1 = new ApplicationRole | ||
{ | ||
Name = StellarRoles.Basic | ||
}; | ||
|
||
_logger.LogInformation("Started seeding 'roles' collection."); | ||
|
||
if (!await _roleManager.RoleExistsAsync(role1.Name)) | ||
{ | ||
var result1 = await _roleManager.CreateAsync(role1); | ||
|
||
if (result1.Succeeded) | ||
_logger.LogInformation($"Added a role to the database with 'ID': {role1.Id}, and 'Name': {role1.Name}."); | ||
} | ||
|
||
_logger.LogInformation("Finished seeding 'roles' collection."); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
src/Server/StellarChat.Server.Api/DAL/Mongo/Seeders/UsersSeeder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
ο»Ώusing StellarChat.Shared.Infrastructure.Identity.Seeders; | ||
|
||
namespace StellarChat.Server.Api.DAL.Mongo.Seeders; | ||
|
||
internal sealed class UsersSeeder(UserManager<ApplicationUser> userManager,ILogger<UsersSeeder> logger) : IUsersSeeder | ||
{ | ||
private readonly UserManager<ApplicationUser> _userManager = userManager; | ||
private readonly ILogger<UsersSeeder> _logger = logger; | ||
|
||
public async Task SeedAsync() | ||
{ | ||
string passwordUserBasic = "Test123!"; | ||
|
||
var user1 = new ApplicationUser | ||
{ | ||
UserName = "user1", | ||
Email = "[email protected]", | ||
FirstName = "User", | ||
LastName = "Demo", | ||
EmailConfirmed = true | ||
}; | ||
|
||
_logger.LogInformation("Started seeding 'users' collection."); | ||
|
||
var result1 = await _userManager.CreateAsync(user1, passwordUserBasic); | ||
|
||
if (result1.Succeeded) | ||
{ | ||
await _userManager.AddToRoleAsync(user1, StellarRoles.Basic); | ||
_logger.LogInformation($"Added a user to the database with 'ID': {user1.Id}, and 'UserName': {user1.UserName}."); | ||
} | ||
|
||
_logger.LogInformation("Finished seeding 'users' collection."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
src/Server/StellarChat.Server.Api/Features/Idenitity/User/GetProfile/GetProfile.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.GetProfile; | ||
|
||
public class GetProfile : ICommand<GetProfileResponse> | ||
{ | ||
} |
30 changes: 30 additions & 0 deletions
30
src/Server/StellarChat.Server.Api/Features/Idenitity/User/GetProfile/GetProfileEndpoint.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.GetProfile; | ||
|
||
internal sealed class GetProfileEndpoint : IEndpoint | ||
{ | ||
public void Expose(IEndpointRouteBuilder endpoints) | ||
{ | ||
var userManagement = endpoints.MapGroup("/user").WithTags("User Management"); | ||
|
||
userManagement.MapGet("/profile", [Authorize] async (IMediator mediator) => | ||
{ | ||
var response = await mediator.Send(new GetProfile()); | ||
|
||
if (!response.Success) | ||
return Results.BadRequest(response); | ||
|
||
return Results.Ok(response); | ||
}) | ||
.Produces(StatusCodes.Status200OK) | ||
.Produces(StatusCodes.Status400BadRequest) | ||
.WithName("GetUserProfile") | ||
.WithOpenApi(operation => new(operation) | ||
{ | ||
Summary = "Retrieves the user profile of the logged in user." | ||
}); | ||
} | ||
|
||
public void Register(IServiceCollection services, IConfiguration configuration) { } | ||
|
||
public void Use(IApplicationBuilder app) { } | ||
} |
43 changes: 43 additions & 0 deletions
43
src/Server/StellarChat.Server.Api/Features/Idenitity/User/GetProfile/GetProfileHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.GetProfile; | ||
|
||
internal class GetProfileHandler : ICommandHandler<GetProfile, GetProfileResponse> | ||
{ | ||
private readonly IHttpContextAccessor _httpContextAccessor; | ||
private readonly UserManager<ApplicationUser> _userManager; | ||
private readonly JwtOptions _jwtOptions; | ||
|
||
public GetProfileHandler( | ||
IHttpContextAccessor httpContextAccessor, | ||
UserManager<ApplicationUser> userManager, | ||
IOptions<JwtOptions> jwtOptions) | ||
{ | ||
_httpContextAccessor = httpContextAccessor; | ||
_userManager = userManager; | ||
_jwtOptions = jwtOptions.Value; | ||
} | ||
|
||
public async ValueTask<GetProfileResponse> Handle(GetProfile command, CancellationToken cancellationToken) | ||
{ | ||
var userId = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); | ||
var user = await _userManager.FindByIdAsync(userId); | ||
|
||
if (user == null) | ||
return new GetProfileResponse { Success = false, Message = "User not found" }; | ||
|
||
var userRoles = await _userManager.GetRolesAsync(user); | ||
|
||
return new GetProfileResponse | ||
{ | ||
Success = true, | ||
UserName = user.UserName, | ||
Email = user.Email, | ||
FirstName = user.FirstName, | ||
LastName = user.LastName, | ||
PhoneNumber = user.PhoneNumber, | ||
IsPhoneNumberConfirmed = user.PhoneNumberConfirmed, | ||
Role = userRoles.FirstOrDefault() | ||
}; | ||
} | ||
|
||
|
||
} |
11 changes: 11 additions & 0 deletions
11
src/Server/StellarChat.Server.Api/Features/Idenitity/User/LoginUser/LoginUser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.LoginUser; | ||
|
||
public class LoginUser : ICommand<LoginUserResponse> | ||
{ | ||
|
||
[Required, EmailAddress] | ||
public string Email { get; set; } = default!; | ||
[Required, DataType(DataType.Password)] | ||
public string Password { get; set; } = default!; | ||
|
||
} |
31 changes: 31 additions & 0 deletions
31
src/Server/StellarChat.Server.Api/Features/Idenitity/User/LoginUser/LoginUserEndpoint.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.LoginUser; | ||
|
||
internal sealed class LoginUserEndpoint : IEndpoint | ||
{ | ||
public void Expose(IEndpointRouteBuilder endpoints) | ||
{ | ||
var userManagement = endpoints.MapGroup("/user").WithTags("User Management"); | ||
|
||
userManagement.MapPost("/login", [AllowAnonymous] async ([FromBody] LoginUserRequest request, IMediator mediator) => | ||
{ | ||
var command = request.Adapt<LoginUser>(); | ||
|
||
var response = await mediator.Send(command); | ||
|
||
if (!response.Success) | ||
return Results.BadRequest(response); | ||
|
||
return Results.Ok(response); | ||
}) | ||
.Produces(StatusCodes.Status200OK) | ||
.Produces(StatusCodes.Status400BadRequest) | ||
.WithOpenApi(operation => new(operation) | ||
{ | ||
Summary = "User login." | ||
}); | ||
} | ||
|
||
public void Register(IServiceCollection services, IConfiguration configuration) { } | ||
|
||
public void Use(IApplicationBuilder app) { } | ||
} |
73 changes: 73 additions & 0 deletions
73
src/Server/StellarChat.Server.Api/Features/Idenitity/User/LoginUser/LoginUserHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.LoginUser; | ||
|
||
internal class LoginUserHandler : ICommandHandler<LoginUser, LoginUserResponse> | ||
{ | ||
private readonly UserManager<ApplicationUser> _userManager; | ||
private readonly JwtOptions _jwtOptions; | ||
|
||
public LoginUserHandler( | ||
UserManager<ApplicationUser> userManager, | ||
IOptions<JwtOptions> jwtOptions) | ||
{ | ||
_userManager = userManager; | ||
_jwtOptions = jwtOptions.Value; | ||
} | ||
|
||
public async ValueTask<LoginUserResponse> Handle(LoginUser command, CancellationToken cancellationToken) | ||
{ | ||
var user = await _userManager.FindByEmailAsync(command.Email); | ||
if (user != null) | ||
{ | ||
if (await _userManager.IsLockedOutAsync(user)) | ||
return new LoginUserResponse(false, "Your account is locked out. Please try again later.", null); | ||
|
||
if (await _userManager.CheckPasswordAsync(user, command.Password)) | ||
{ | ||
await _userManager.ResetAccessFailedCountAsync(user); | ||
|
||
var roles = await _userManager.GetRolesAsync(user); | ||
var userRole = roles.FirstOrDefault(); | ||
|
||
var tokenHandler = new JwtSecurityTokenHandler(); | ||
var secretKey = _jwtOptions.SECRET_KEY; | ||
|
||
if (string.IsNullOrEmpty(secretKey)) | ||
return new LoginUserResponse(false, "Internal server error", null); | ||
|
||
var key = Convert.FromBase64String(secretKey); | ||
var tokenDescriptor = new SecurityTokenDescriptor | ||
{ | ||
Subject = new ClaimsIdentity( | ||
[ | ||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), | ||
new Claim(ClaimTypes.Name, user.UserName ?? string.Empty), | ||
new Claim(ClaimTypes.Role, userRole ?? string.Empty), | ||
new Claim("scope", "api1") | ||
]), | ||
|
||
Expires = DateTime.UtcNow.AddHours(1), | ||
Issuer = _jwtOptions.ISSUER, | ||
Audience = _jwtOptions.ISSUER, | ||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) | ||
}; | ||
var token = tokenHandler.CreateToken(tokenDescriptor); | ||
var tokenString = tokenHandler.WriteToken(token); | ||
|
||
return new LoginUserResponse(true, string.Empty, tokenString); | ||
} | ||
else | ||
{ | ||
await _userManager.AccessFailedAsync(user); | ||
|
||
if (await _userManager.IsLockedOutAsync(user)) | ||
return new LoginUserResponse(false, "Your account is locked out. Please try again later.", null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of returning a record for error handling, consider using custom exceptions. This approach will help to standardize error handling across the application and make it easier to manage and trace exceptions. |
||
|
||
return new LoginUserResponse(false, "Invalid login attempt.", null); | ||
} | ||
} | ||
|
||
return new LoginUserResponse(false, "Unauthorized", null); | ||
} | ||
|
||
|
||
} |
14 changes: 14 additions & 0 deletions
14
...erver/StellarChat.Server.Api/Features/Idenitity/User/RegistrationUser/RegistrationUser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.RegistrationUser; | ||
|
||
public class RegistrationUser : ICommand<RegistrationUserResponse> | ||
{ | ||
public string Username { get; set; } = default!; | ||
|
||
[Required, EmailAddress] | ||
public string Email { get; set; } = default!; | ||
[Required, DataType(DataType.Password)] | ||
public string Password { get; set; } = default!; | ||
[Required, DataType(DataType.Password), Compare(nameof(Password), ErrorMessage = "Passwords do not match")] | ||
public string ConfirmPassword { get; set; } = default!; | ||
|
||
} |
31 changes: 31 additions & 0 deletions
31
...ellarChat.Server.Api/Features/Idenitity/User/RegistrationUser/RegistrationUserEndpoint.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.RegistrationUser; | ||
|
||
internal sealed class RegistrationUserEndpoint : IEndpoint | ||
{ | ||
public void Expose(IEndpointRouteBuilder endpoints) | ||
{ | ||
var userManagement = endpoints.MapGroup("/user").WithTags("User Management"); | ||
|
||
userManagement.MapPost("/register", [AllowAnonymous] async ([FromBody] RegistrationUserRequest request, IMediator mediator) => | ||
{ | ||
var command = request.Adapt<RegistrationUser>(); | ||
|
||
var response = await mediator.Send(command); | ||
|
||
if (!response.Success) | ||
return Results.BadRequest(response); | ||
|
||
return Results.Ok(response); | ||
}) | ||
.Produces(StatusCodes.Status201Created) | ||
.Produces(StatusCodes.Status400BadRequest) | ||
.WithOpenApi(operation => new(operation) | ||
{ | ||
Summary = "Registration new anonimus user." | ||
}); | ||
} | ||
|
||
public void Register(IServiceCollection services, IConfiguration configuration) { } | ||
|
||
public void Use(IApplicationBuilder app) { } | ||
} |
45 changes: 45 additions & 0 deletions
45
...tellarChat.Server.Api/Features/Idenitity/User/RegistrationUser/RegistrationUserHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
ο»Ώnamespace StellarChat.Server.Api.Features.Idenitity.User.RegistrationUser; | ||
|
||
internal class RegistrationUserHandler : ICommandHandler<RegistrationUser, RegistrationUserResponse> | ||
{ | ||
private readonly UserManager<ApplicationUser> _userManager; | ||
private readonly ILogger<RegistrationUserHandler> _logger; | ||
|
||
public RegistrationUserHandler( | ||
UserManager<ApplicationUser> userManager, | ||
ILogger<RegistrationUserHandler> logger) | ||
{ | ||
_userManager = userManager; | ||
_logger = logger; | ||
} | ||
|
||
public async ValueTask<RegistrationUserResponse> Handle(RegistrationUser command, CancellationToken cancellationToken) | ||
{ | ||
_logger.LogInformation("Registering a user!"); | ||
|
||
var user = new ApplicationUser { UserName = command.Username, Email = command.Email, RegistredOn = DateTime.Now }; | ||
var result = await _userManager.CreateAsync(user, command.Password); | ||
|
||
if (result.Succeeded) | ||
{ | ||
await _userManager.AddToRoleAsync(user, StellarRoles.Basic); | ||
|
||
return new RegistrationUserResponse( | ||
Success: true, | ||
"User registered successfully", | ||
user.UserName, | ||
user.Email); | ||
} | ||
var errorDescriptions = string.Join(", ", result.Errors.Select(e => e.Description)); | ||
|
||
_logger.LogWarning($"User registration failed. {errorDescriptions}"); | ||
|
||
return new RegistrationUserResponse( | ||
Success: false, | ||
$"User registration failed. {errorDescriptions}", | ||
null, | ||
null); | ||
} | ||
|
||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using IContext interface from the Shared project instead of IHttpContextAccessor. IContext is already designed to handle request information and the authenticated user, so it might be a more consistent approach across the codebase.