Resolve "Crud API endpoints" #43

Merged
leon.hoppe merged 2 commits from feature/crud into dev 2024-12-22 19:13:36 +01:00
9 changed files with 226 additions and 5 deletions
Showing only changes of commit ae74745108 - Show all commits

View File

@@ -0,0 +1,83 @@
using HopFrame.Api.Logic;
using HopFrame.Api.Models;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace HopFrame.Api.Controller;
[ApiController, Route("api/v1/users")]
public class UserController(IOptions<AdminPermissionOptions> permissions, IPermissionRepository perms, ITokenContext context, IUserLogic logic) : ControllerBase {
private async Task<bool> AuthorizeRequest(string permission) {
return await perms.HasPermission(context.AccessToken, permission);
}
[HttpGet, Authorized]
public async Task<ActionResult<IList<User>>> GetUsers() {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUsers();
}
[HttpGet("{userId}"), Authorized]
public async Task<ActionResult<User>> GetUser(string userId) {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUser(userId);
}
[HttpGet("username/{username}"), Authorized]
public async Task<ActionResult<User>> GetUserByUsername(string username) {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUserByUsername(username);
}
[HttpGet("email/{email}"), Authorized]
public async Task<ActionResult<User>> GetUserByEmail(string email) {
if (!await AuthorizeRequest(permissions.Value.Users.Read))
return Unauthorized();
return await logic.GetUserByEmail(email);
}
[HttpPost, Authorized]
public async Task<ActionResult<User>> CreateUser([FromBody] UserCreator user) {
if (!await AuthorizeRequest(permissions.Value.Users.Create))
return Unauthorized();
return await logic.CreateUser(user);
}
[HttpPut("{userId}"), Authorized]
public async Task<ActionResult<User>> UpdateUser(string userId, [FromBody] User user) {
if (!await AuthorizeRequest(permissions.Value.Users.Update))
return Unauthorized();
return await logic.UpdateUser(userId, user);
}
[HttpDelete("{userId}"), Authorized]
public async Task<ActionResult> DeleteUser(string userId) {
if (!await AuthorizeRequest(permissions.Value.Users.Delete))
return Unauthorized();
return await logic.DeleteUser(userId);
}
[HttpPut("{userId}/password"), Authorized]
public async Task<ActionResult> ChangePassword(string userId, [FromBody] UserPasswordChange passwordChange) {
if (context.User.Id.ToString() != userId && !await AuthorizeRequest(permissions.Value.Users.Update))
return Unauthorized();
return await logic.UpdatePassword(userId, passwordChange.OldPassword, passwordChange.NewPassword);
}
}

View File

@@ -19,9 +19,10 @@ public static class ServiceCollectionExtensions {
/// <param name="configuration">The configuration used to configure HopFrame authentication</param> /// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
var controllers = new List<Type>(); var controllers = new List<Type> { typeof(UserController) };
if (configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication")) var defaultAuthenticationSection = configuration.GetSection("HopFrame:Authentication:DefaultAuthentication");
if (!defaultAuthenticationSection.Exists() || configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication"))
controllers.Add(typeof(AuthController)); controllers.Add(typeof(AuthController));
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled")) if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled"))
@@ -46,6 +47,7 @@ public static class ServiceCollectionExtensions {
services.AddHopFrameRepositories<TDbContext>(); services.AddHopFrameRepositories<TDbContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthLogic, AuthLogic>(); services.AddScoped<IAuthLogic, AuthLogic>();
services.AddScoped<IUserLogic, UserLogic>();
services.AddHopFrameAuthentication(configuration); services.AddHopFrameAuthentication(configuration);
} }

View File

@@ -1,4 +1,5 @@
using System.Net; using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Api.Logic; namespace HopFrame.Api.Logic;

View File

@@ -0,0 +1,16 @@
using HopFrame.Api.Models;
using HopFrame.Database.Models;
namespace HopFrame.Api.Logic;
public interface IUserLogic {
Task<LogicResult<IList<User>>> GetUsers();
Task<LogicResult<User>> GetUser(string id);
Task<LogicResult<User>> GetUserByUsername(string username);
Task<LogicResult<User>> GetUserByEmail(string email);
Task<LogicResult<User>> CreateUser(UserCreator user);
Task<LogicResult<User>> UpdateUser(string id, User user);
Task<LogicResult> DeleteUser(string id);
Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword);
}

View File

@@ -9,7 +9,7 @@ using Microsoft.Extensions.Options;
namespace HopFrame.Api.Logic.Implementation; namespace HopFrame.Api.Logic.Implementation;
internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic { internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic {
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) { public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled"); if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");

View File

@@ -0,0 +1,105 @@
using HopFrame.Api.Models;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Claims;
namespace HopFrame.Api.Logic.Implementation;
internal sealed class UserLogic(IUserRepository users, ITokenContext context) : IUserLogic {
public async Task<LogicResult<IList<User>>> GetUsers() {
return LogicResult<IList<User>>.Ok(await users.GetUsers());
}
public async Task<LogicResult<User>> GetUser(string id) {
if (!Guid.TryParse(id, out var userId))
return LogicResult<User>.BadRequest("Invalid user id");
var user = await users.GetUser(userId);
if (user is null)
return LogicResult<User>.NotFound("That user does not exist");
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult<User>> GetUserByUsername(string username) {
var user = await users.GetUserByUsername(username);
if (user is null)
return LogicResult<User>.NotFound("That user does not exist");
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult<User>> GetUserByEmail(string email) {
var user = await users.GetUserByEmail(email);
if (user is null)
return LogicResult<User>.NotFound("That user does not exist");
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult<User>> CreateUser(UserCreator user) {
var createdUser = new User {
Email = user.Email,
Username = user.Username,
Password = user.Password,
};
createdUser.Permissions = user.Permissions?.Select(p => new Permission {
GrantedAt = DateTime.Now,
PermissionName = p,
User = createdUser
}).ToList();
var newUser = await users.AddUser(createdUser);
if (newUser is null)
return LogicResult<User>.Conflict("That user already exists");
return LogicResult<User>.Ok(newUser);
}
public async Task<LogicResult<User>> UpdateUser(string id, User user) {
if (!Guid.TryParse(id, out var userId))
return LogicResult<User>.BadRequest("Invalid user id");
if (user.Id != userId)
return LogicResult<User>.Conflict("Cannot edit user with different user id");
if (await users.GetUser(userId) is null)
return LogicResult<User>.NotFound("That user does not exist");
await users.UpdateUser(user);
return LogicResult<User>.Ok(user);
}
public async Task<LogicResult> DeleteUser(string id) {
if (!Guid.TryParse(id, out var userId))
return LogicResult.BadRequest("Invalid user id");
var user = await users.GetUser(userId);
if (user is null)
return LogicResult.NotFound("That user does not exist");
await users.DeleteUser(user);
return LogicResult.Ok();
}
public async Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword) {
if (!Guid.TryParse(id, out var userId))
return LogicResult.BadRequest("Invalid user id");
var user = await users.GetUser(userId);
if (user is null)
return LogicResult.NotFound("That user does not exist");
if (userId == context.User.Id && !await users.CheckUserPassword(user, oldPassword))
return LogicResult.Conflict("Old password is not correct");
await users.ChangePassword(user, newPassword);
return LogicResult.Ok();
}
}

View File

@@ -0,0 +1,8 @@
namespace HopFrame.Api.Models;
public class UserCreator {
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual List<string> Permissions { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Api.Models;
public class UserPasswordChange {
public string OldPassword { get; set; }
public string NewPassword { get; set; }
}

View File

@@ -5,7 +5,7 @@ namespace HopFrame.Database.Models;
public class User : IPermissionOwner { public class User : IPermissionOwner {
[Key, Required, MinLength(36), MaxLength(36)] [Key, Required]
public Guid Id { get; init; } public Guid Id { get; init; }
[Required, MaxLength(50)] [Required, MaxLength(50)]
@@ -14,7 +14,7 @@ public class User : IPermissionOwner {
[Required, MaxLength(50), EmailAddress] [Required, MaxLength(50), EmailAddress]
public string Email { get; set; } public string Email { get; set; }
[Required, MinLength(8), MaxLength(255), JsonIgnore] [MinLength(8), MaxLength(255), JsonIgnore]
public string Password { get; set; } public string Password { get; set; }
[Required] [Required]