From 76cb56d819ba9192d376faa81bb37f803dce693b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vrabec?= Date: Sun, 10 Aug 2025 11:20:18 +0200 Subject: [PATCH 01/14] Add user authorization service --- DrinkRateAPI/Exceptions/Exceptions.cs | 7 +++++++ .../Services/ApplicationUserService.cs | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 DrinkRateAPI/Exceptions/Exceptions.cs create mode 100644 DrinkRateAPI/Services/ApplicationUserService.cs diff --git a/DrinkRateAPI/Exceptions/Exceptions.cs b/DrinkRateAPI/Exceptions/Exceptions.cs new file mode 100644 index 0000000..14c8432 --- /dev/null +++ b/DrinkRateAPI/Exceptions/Exceptions.cs @@ -0,0 +1,7 @@ +namespace DrinkRateAPI.Exceptions; + +public class DrinkRateException : Exception; + +public class NotFoundException : DrinkRateException; + +public class UnauthorizedException : DrinkRateException; \ No newline at end of file diff --git a/DrinkRateAPI/Services/ApplicationUserService.cs b/DrinkRateAPI/Services/ApplicationUserService.cs new file mode 100644 index 0000000..a2f735d --- /dev/null +++ b/DrinkRateAPI/Services/ApplicationUserService.cs @@ -0,0 +1,21 @@ +using System.Security.Claims; +using DrinkRateAPI.Contexts; +using DrinkRateAPI.DbEntities; +using DrinkRateAPI.Exceptions; +using Microsoft.EntityFrameworkCore; + +namespace DrinkRateAPI.Services; + +public class ApplicationUserService(ApplicationDbContext context) +{ + private ApplicationDbContext _context = context; + public async Task UserProfileByApplicationUserAsync(ClaimsPrincipal identity) + { + var appUserId = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var profile = await _context.UserProfiles + .FirstAsync(x => x.ApplicationUserId.ToString() == appUserId) + ?? throw new NotFoundException(); + + return profile; + } +} \ No newline at end of file From c0860b05d100b63e9961f9f3c539fa9d7545dbbc Mon Sep 17 00:00:00 2001 From: martinshoob Date: Sun, 10 Aug 2025 13:55:20 +0200 Subject: [PATCH 02/14] Enable admin status management Adds functionality to allow administrators to modify the admin status of other users. Introduces an endpoint for changing user admin status, accessible only to existing administrators. This change includes necessary services and request models to handle the logic. --- DrinkRateAPI/Controllers/AdminController.cs | 38 +++++++++++++++++++ DrinkRateAPI/Program.cs | 3 ++ .../Requests/ChangeUserAdminStatusRequest.cs | 7 ++++ DrinkRateAPI/Services/UserProfileService.cs | 34 +++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 DrinkRateAPI/Controllers/AdminController.cs create mode 100644 DrinkRateAPI/Requests/ChangeUserAdminStatusRequest.cs create mode 100644 DrinkRateAPI/Services/UserProfileService.cs diff --git a/DrinkRateAPI/Controllers/AdminController.cs b/DrinkRateAPI/Controllers/AdminController.cs new file mode 100644 index 0000000..6c42c67 --- /dev/null +++ b/DrinkRateAPI/Controllers/AdminController.cs @@ -0,0 +1,38 @@ +using DrinkRateAPI.Requests; +using DrinkRateAPI.Services; +using Microsoft.AspNetCore.Mvc; + +namespace DrinkRateAPI.Controllers; + +[ApiController] +[Route("[controller]")] +public class AdminController : ControllerBase +{ + private readonly ILogger _logger; + private readonly ApplicationUserService _applicationUserService; + private readonly UserProfileService _userProfileService; + + public AdminController(ILogger logger, ApplicationUserService applicationUserService, + UserProfileService userProfileService) + { + _logger = logger; + _applicationUserService = applicationUserService; + _userProfileService = userProfileService; + } + + [HttpPut] + [Route("[action]")] + [Produces("application/json")] + public async Task PutUserAdminStatus([FromBody] ChangeUserAdminStatusRequest request) + { + var userProfile = await _applicationUserService.UserProfileByApplicationUserAsync(User); + if (!_userProfileService.IsUserProfileAdmin(userProfile)) + { + return Unauthorized(); + } + + var changedProfile = _userProfileService.ChangeUserAdminStatus(request.UserId, request.ChangeStatusTo); + + return Ok(changedProfile); + } +} diff --git a/DrinkRateAPI/Program.cs b/DrinkRateAPI/Program.cs index 1faf6d8..acdcb89 100644 --- a/DrinkRateAPI/Program.cs +++ b/DrinkRateAPI/Program.cs @@ -1,5 +1,6 @@ using DrinkRateAPI.Contexts; using DrinkRateAPI.DbEntities; +using DrinkRateAPI.Services; using Microsoft.AspNetCore.Identity; using Microsoft.OpenApi.Models; @@ -50,6 +51,8 @@ builder.Services.AddSwaggerGen(c => }); builder.Services.AddDbContext(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/DrinkRateAPI/Requests/ChangeUserAdminStatusRequest.cs b/DrinkRateAPI/Requests/ChangeUserAdminStatusRequest.cs new file mode 100644 index 0000000..03ac6f2 --- /dev/null +++ b/DrinkRateAPI/Requests/ChangeUserAdminStatusRequest.cs @@ -0,0 +1,7 @@ +namespace DrinkRateAPI.Requests; + +public class ChangeUserAdminStatusRequest +{ + public required string UserId { get; set; } + public bool ChangeStatusTo { get; set; } +} \ No newline at end of file diff --git a/DrinkRateAPI/Services/UserProfileService.cs b/DrinkRateAPI/Services/UserProfileService.cs new file mode 100644 index 0000000..7daa187 --- /dev/null +++ b/DrinkRateAPI/Services/UserProfileService.cs @@ -0,0 +1,34 @@ +using System.Security.Claims; +using DrinkRateAPI.Contexts; +using DrinkRateAPI.DbEntities; +using DrinkRateAPI.Exceptions; +using Microsoft.EntityFrameworkCore; + +namespace DrinkRateAPI.Services; + +public class UserProfileService(ApplicationDbContext context) +{ + private ApplicationDbContext _context = context; + + public bool IsUserProfileAdmin(DbUserProfile userProfile) + { + return userProfile.IsAdmin; + } + + public DbUserProfile ChangeUserAdminStatus(string userId, bool changeStatusTo) + { + var userProfile = GetUserProfileById(userId); + userProfile.IsAdmin = changeStatusTo; + _context.UserProfiles.Update(userProfile); + _context.SaveChanges(); + + return userProfile; + } + + public DbUserProfile GetUserProfileById(string userId) + { + var userProfile = _context.UserProfiles.FirstOrDefault(x => x.Id.ToString() == userId); + + return userProfile ?? throw new KeyNotFoundException($"User with ID {userId} not found"); + } +} \ No newline at end of file From b2b8d1e076df997b856efcaa33d1904eabc749aa Mon Sep 17 00:00:00 2001 From: martinshoob Date: Sun, 10 Aug 2025 16:23:35 +0200 Subject: [PATCH 03/14] Refactor admin endpoint and logic Moves the ChangeUserAdminStatusRequest to the ApiModels folder. Updates the admin controller route to "admin" and the admin status endpoint to "adminStatus". Makes the ChangeUserAdminStatus method asynchronous. Uses NotFoundException instead of KeyNotFoundException. --- .../UserProfile}/ChangeUserAdminStatusRequest.cs | 2 +- DrinkRateAPI/Controllers/AdminController.cs | 8 ++++---- DrinkRateAPI/DrinkRateAPI.csproj | 1 + DrinkRateAPI/Services/UserProfileService.cs | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) rename DrinkRateAPI/{Requests => ApiModels/UserProfile}/ChangeUserAdminStatusRequest.cs (75%) diff --git a/DrinkRateAPI/Requests/ChangeUserAdminStatusRequest.cs b/DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs similarity index 75% rename from DrinkRateAPI/Requests/ChangeUserAdminStatusRequest.cs rename to DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs index 03ac6f2..e28228d 100644 --- a/DrinkRateAPI/Requests/ChangeUserAdminStatusRequest.cs +++ b/DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs @@ -1,4 +1,4 @@ -namespace DrinkRateAPI.Requests; +namespace DrinkRateAPI.ApiModels.UserProfile; public class ChangeUserAdminStatusRequest { diff --git a/DrinkRateAPI/Controllers/AdminController.cs b/DrinkRateAPI/Controllers/AdminController.cs index 6c42c67..59223f5 100644 --- a/DrinkRateAPI/Controllers/AdminController.cs +++ b/DrinkRateAPI/Controllers/AdminController.cs @@ -1,11 +1,11 @@ -using DrinkRateAPI.Requests; +using DrinkRateAPI.ApiModels.UserProfile; using DrinkRateAPI.Services; using Microsoft.AspNetCore.Mvc; namespace DrinkRateAPI.Controllers; [ApiController] -[Route("[controller]")] +[Route("admin")] public class AdminController : ControllerBase { private readonly ILogger _logger; @@ -21,7 +21,7 @@ public class AdminController : ControllerBase } [HttpPut] - [Route("[action]")] + [Route("adminStatus")] [Produces("application/json")] public async Task PutUserAdminStatus([FromBody] ChangeUserAdminStatusRequest request) { @@ -31,7 +31,7 @@ public class AdminController : ControllerBase return Unauthorized(); } - var changedProfile = _userProfileService.ChangeUserAdminStatus(request.UserId, request.ChangeStatusTo); + var changedProfile = await _userProfileService.ChangeUserAdminStatusAsync(request.UserId, request.ChangeStatusTo); return Ok(changedProfile); } diff --git a/DrinkRateAPI/DrinkRateAPI.csproj b/DrinkRateAPI/DrinkRateAPI.csproj index dacaac5..7cdbba1 100644 --- a/DrinkRateAPI/DrinkRateAPI.csproj +++ b/DrinkRateAPI/DrinkRateAPI.csproj @@ -20,6 +20,7 @@ + diff --git a/DrinkRateAPI/Services/UserProfileService.cs b/DrinkRateAPI/Services/UserProfileService.cs index 7daa187..1eec317 100644 --- a/DrinkRateAPI/Services/UserProfileService.cs +++ b/DrinkRateAPI/Services/UserProfileService.cs @@ -15,12 +15,12 @@ public class UserProfileService(ApplicationDbContext context) return userProfile.IsAdmin; } - public DbUserProfile ChangeUserAdminStatus(string userId, bool changeStatusTo) + public async Task ChangeUserAdminStatusAsync(string userId, bool changeStatusTo) { var userProfile = GetUserProfileById(userId); userProfile.IsAdmin = changeStatusTo; _context.UserProfiles.Update(userProfile); - _context.SaveChanges(); + await _context.SaveChangesAsync(); return userProfile; } @@ -29,6 +29,6 @@ public class UserProfileService(ApplicationDbContext context) { var userProfile = _context.UserProfiles.FirstOrDefault(x => x.Id.ToString() == userId); - return userProfile ?? throw new KeyNotFoundException($"User with ID {userId} not found"); + return userProfile ?? throw new NotFoundException(); } } \ No newline at end of file From dad144a80fdb3206466f9cf62424fbe90a049fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vrabec?= Date: Sun, 10 Aug 2025 16:35:36 +0200 Subject: [PATCH 04/14] Create user profile service base --- .../ApiModels/UserProfile/UserProfileGet.cs | 6 +++++ .../ApiModels/UserProfile/UserProfilePut.cs | 6 +++++ .../Controllers/UserProfileController.cs | 18 ++++++++++++++ DrinkRateAPI/Services/UserProfileService.cs | 24 +++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs create mode 100644 DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs create mode 100644 DrinkRateAPI/Controllers/UserProfileController.cs create mode 100644 DrinkRateAPI/Services/UserProfileService.cs diff --git a/DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs b/DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs new file mode 100644 index 0000000..ed241cf --- /dev/null +++ b/DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs @@ -0,0 +1,6 @@ +namespace DrinkRateAPI.ApiModels.UserProfile; + +public class UserProfileGet +{ + +} \ No newline at end of file diff --git a/DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs b/DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs new file mode 100644 index 0000000..020071d --- /dev/null +++ b/DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs @@ -0,0 +1,6 @@ +namespace DrinkRateAPI.ApiModels.UserProfile; + +public class UserProfilePut +{ + public string UserName { get; set; } +} \ No newline at end of file diff --git a/DrinkRateAPI/Controllers/UserProfileController.cs b/DrinkRateAPI/Controllers/UserProfileController.cs new file mode 100644 index 0000000..3405fd6 --- /dev/null +++ b/DrinkRateAPI/Controllers/UserProfileController.cs @@ -0,0 +1,18 @@ +using System.Security.Claims; +using DrinkRateAPI.ApiModels.UserProfile; +using Microsoft.AspNetCore.Mvc; + +namespace DrinkRateAPI.Controllers; + +[ApiController] +[Route("user_profile")] +public class UserProfileController : ControllerBase +{ + [HttpPut(Name = "user_profile")] + public UserProfileGet PutUserProfile(UserProfilePut userProfile) + { + throw new ApplicationException(); + var x = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; //HttpContext.User.Identities.First(); + return new(); + } +} \ No newline at end of file diff --git a/DrinkRateAPI/Services/UserProfileService.cs b/DrinkRateAPI/Services/UserProfileService.cs new file mode 100644 index 0000000..3bf12f8 --- /dev/null +++ b/DrinkRateAPI/Services/UserProfileService.cs @@ -0,0 +1,24 @@ +using System.Security.Claims; +using DrinkRateAPI.ApiModels.UserProfile; +using DrinkRateAPI.Contexts; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + + +namespace DrinkRateAPI.Services; + +public class UserProfileService(ApplicationDbContext context, + ApplicationUserService applicationUserService) +{ + private ApplicationDbContext _context = context; + private ApplicationUserService _applicationUserService = applicationUserService; + + public async Task PutUserProfileAsync(UserProfilePut userProfile, ClaimsPrincipal identity) + { + var profile = _applicationUserService.UserProfileByApplicationUserAsync(identity); + + + + return new(); + } +} \ No newline at end of file From b59fef222f5bdd3328f2f3af0e3c14294b18b4d1 Mon Sep 17 00:00:00 2001 From: martinshoob Date: Sun, 10 Aug 2025 18:07:34 +0200 Subject: [PATCH 05/14] Implement admin-only authorization policy Adds an authorization policy to restrict access to admin-only endpoints. Creates an `AdminOnlyRequirement` and `AdminOnlyHandler` to check if a user has admin privileges. Applies the "AdminOnly" policy to the AdminController to secure admin functionalities. Modifies the endpoint for changing user admin status to include the user ID in the route. --- ...tusRequest.cs => ChangeAdminStatusBody.cs} | 3 +- .../AdminOnlyRequirement.cs | 49 +++++++++++++++++++ DrinkRateAPI/Controllers/AdminController.cs | 22 +++------ DrinkRateAPI/Program.cs | 30 +++++++----- 4 files changed, 74 insertions(+), 30 deletions(-) rename DrinkRateAPI/ApiModels/UserProfile/{ChangeUserAdminStatusRequest.cs => ChangeAdminStatusBody.cs} (51%) create mode 100644 DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs diff --git a/DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs b/DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs similarity index 51% rename from DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs rename to DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs index e28228d..238546f 100644 --- a/DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs +++ b/DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs @@ -1,7 +1,6 @@ namespace DrinkRateAPI.ApiModels.UserProfile; -public class ChangeUserAdminStatusRequest +public class ChangeAdminStatusBody { - public required string UserId { get; set; } public bool ChangeStatusTo { get; set; } } \ No newline at end of file diff --git a/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs b/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs new file mode 100644 index 0000000..e972e3b --- /dev/null +++ b/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs @@ -0,0 +1,49 @@ +using DrinkRateAPI.DbEntities; +using DrinkRateAPI.Services; + +namespace DrinkRateAPI.AuthorizationPolicies; +using Microsoft.AspNetCore.Authorization; + +public class AdminOnlyRequirement : IAuthorizationRequirement +{ +} + +public class AdminOnlyHandler : AuthorizationHandler +{ + private readonly ApplicationUserService _applicationUserService; + private readonly UserProfileService _userProfileService; + + public AdminOnlyHandler( + ApplicationUserService applicationUserService, + UserProfileService userProfileService) + { + _applicationUserService = applicationUserService; + _userProfileService = userProfileService; + } + + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + AdminOnlyRequirement requirement) + { + DbUserProfile userProfile; + + try + { + userProfile = await _applicationUserService.UserProfileByApplicationUserAsync(context.User); + } + catch (Exception _) + { + context.Fail(); + return; + } + + if (_userProfileService.IsUserProfileAdmin(userProfile)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } +} \ No newline at end of file diff --git a/DrinkRateAPI/Controllers/AdminController.cs b/DrinkRateAPI/Controllers/AdminController.cs index 59223f5..4bbc46d 100644 --- a/DrinkRateAPI/Controllers/AdminController.cs +++ b/DrinkRateAPI/Controllers/AdminController.cs @@ -1,38 +1,30 @@ using DrinkRateAPI.ApiModels.UserProfile; using DrinkRateAPI.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace DrinkRateAPI.Controllers; [ApiController] [Route("admin")] +[Authorize(Policy = "AdminOnly")] public class AdminController : ControllerBase { private readonly ILogger _logger; - private readonly ApplicationUserService _applicationUserService; private readonly UserProfileService _userProfileService; - public AdminController(ILogger logger, ApplicationUserService applicationUserService, - UserProfileService userProfileService) + public AdminController(ILogger logger, UserProfileService userProfileService) { _logger = logger; - _applicationUserService = applicationUserService; _userProfileService = userProfileService; } - [HttpPut] - [Route("adminStatus")] + [HttpPut("users/{userId}/adminStatus")] [Produces("application/json")] - public async Task PutUserAdminStatus([FromBody] ChangeUserAdminStatusRequest request) + public async Task PutUserAdminStatus(string userId, [FromBody] ChangeAdminStatusBody body) { - var userProfile = await _applicationUserService.UserProfileByApplicationUserAsync(User); - if (!_userProfileService.IsUserProfileAdmin(userProfile)) - { - return Unauthorized(); - } + var changedProfile = await _userProfileService.ChangeUserAdminStatusAsync(userId, body.ChangeStatusTo); - var changedProfile = await _userProfileService.ChangeUserAdminStatusAsync(request.UserId, request.ChangeStatusTo); - return Ok(changedProfile); } -} +} \ No newline at end of file diff --git a/DrinkRateAPI/Program.cs b/DrinkRateAPI/Program.cs index acdcb89..051ce01 100644 --- a/DrinkRateAPI/Program.cs +++ b/DrinkRateAPI/Program.cs @@ -1,6 +1,8 @@ +using DrinkRateAPI.AuthorizationPolicies; using DrinkRateAPI.Contexts; using DrinkRateAPI.DbEntities; using DrinkRateAPI.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.OpenApi.Models; @@ -11,10 +13,13 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddAuthorization(); +builder.Services.AddAuthorizationBuilder() + .AddPolicy("AdminOnly", policy => + policy.Requirements.Add(new AdminOnlyRequirement())); builder.Services.AddIdentityApiEndpoints() .AddEntityFrameworkStores(); builder.Services.AddScoped, UserWithProfileManager>(); +builder.Services.AddScoped(); builder.Services.AddSwaggerGen(c => { @@ -32,20 +37,19 @@ builder.Services.AddSwaggerGen(c => c.AddSecurityRequirement(new OpenApiSecurityRequirement() { - { - new OpenApiSecurityScheme { - Reference = new OpenApiReference + new OpenApiSecurityScheme { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header, - - }, - new List() + new List() } }); }); @@ -72,4 +76,4 @@ app.UseAuthorization(); app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file From a246764e44953ce75fefce26cde8b0d9f2d3255f Mon Sep 17 00:00:00 2001 From: Martin Velebil Date: Sun, 10 Aug 2025 18:33:36 +0200 Subject: [PATCH 06/14] Movesadmin status update endpoint Relocates the endpoint for updating user admin status from the dedicated AdminController to the UserProfileController. This consolidates user profile management under a single controller and leverages existing authorization policies. --- DrinkRateAPI/Controllers/AdminController.cs | 30 ------------------- .../Controllers/UserProfileController.cs | 23 +++++++++++++- DrinkRateAPI/Services/UserProfileService.cs | 5 ++-- 3 files changed, 24 insertions(+), 34 deletions(-) delete mode 100644 DrinkRateAPI/Controllers/AdminController.cs diff --git a/DrinkRateAPI/Controllers/AdminController.cs b/DrinkRateAPI/Controllers/AdminController.cs deleted file mode 100644 index 4bbc46d..0000000 --- a/DrinkRateAPI/Controllers/AdminController.cs +++ /dev/null @@ -1,30 +0,0 @@ -using DrinkRateAPI.ApiModels.UserProfile; -using DrinkRateAPI.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace DrinkRateAPI.Controllers; - -[ApiController] -[Route("admin")] -[Authorize(Policy = "AdminOnly")] -public class AdminController : ControllerBase -{ - private readonly ILogger _logger; - private readonly UserProfileService _userProfileService; - - public AdminController(ILogger logger, UserProfileService userProfileService) - { - _logger = logger; - _userProfileService = userProfileService; - } - - [HttpPut("users/{userId}/adminStatus")] - [Produces("application/json")] - public async Task PutUserAdminStatus(string userId, [FromBody] ChangeAdminStatusBody body) - { - var changedProfile = await _userProfileService.ChangeUserAdminStatusAsync(userId, body.ChangeStatusTo); - - return Ok(changedProfile); - } -} \ No newline at end of file diff --git a/DrinkRateAPI/Controllers/UserProfileController.cs b/DrinkRateAPI/Controllers/UserProfileController.cs index 3405fd6..afc1bcf 100644 --- a/DrinkRateAPI/Controllers/UserProfileController.cs +++ b/DrinkRateAPI/Controllers/UserProfileController.cs @@ -1,13 +1,24 @@ using System.Security.Claims; using DrinkRateAPI.ApiModels.UserProfile; +using DrinkRateAPI.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace DrinkRateAPI.Controllers; [ApiController] -[Route("user_profile")] +[Route("userProfile")] public class UserProfileController : ControllerBase { + private readonly ILogger _logger; + private readonly UserProfileService _userProfileService; + + public UserProfileController(ILogger logger, UserProfileService userProfileService) + { + _logger = logger; + _userProfileService = userProfileService; + } + [HttpPut(Name = "user_profile")] public UserProfileGet PutUserProfile(UserProfilePut userProfile) { @@ -15,4 +26,14 @@ public class UserProfileController : ControllerBase var x = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; //HttpContext.User.Identities.First(); return new(); } + + [HttpPut("{userId}/adminStatus")] + [Authorize(Policy = "AdminOnly")] + [Produces("application/json")] + public async Task PutUserAdminStatus(string userId, [FromBody] ChangeAdminStatusBody body) + { + var changedProfile = await _userProfileService.PutUserProfileAdminStatusAsync(userId, body.ChangeStatusTo); + + return Ok(changedProfile); + } } \ No newline at end of file diff --git a/DrinkRateAPI/Services/UserProfileService.cs b/DrinkRateAPI/Services/UserProfileService.cs index 96b1091..8726f93 100644 --- a/DrinkRateAPI/Services/UserProfileService.cs +++ b/DrinkRateAPI/Services/UserProfileService.cs @@ -11,8 +11,7 @@ using Microsoft.AspNetCore.Mvc; namespace DrinkRateAPI.Services; -public class UserProfileService(ApplicationDbContext context, - ApplicationUserService applicationUserService) +public class UserProfileService(ApplicationDbContext context, ApplicationUserService applicationUserService) { private ApplicationDbContext _context = context; private ApplicationUserService _applicationUserService = applicationUserService; @@ -22,7 +21,7 @@ public class UserProfileService(ApplicationDbContext context, return userProfile.IsAdmin; } - public async Task ChangeUserAdminStatusAsync(string userId, bool changeStatusTo) + public async Task PutUserProfileAdminStatusAsync(string userId, bool changeStatusTo) { var userProfile = GetUserProfileById(userId); userProfile.IsAdmin = changeStatusTo; From 8058add053b4b900a6c252580f7311ce801a4bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vrabec?= Date: Mon, 11 Aug 2025 07:46:14 +0200 Subject: [PATCH 07/14] Rename class --- ...geAdminStatusBody.cs => UserProfileAdminStatusPut.cs} | 2 +- DrinkRateAPI/Controllers/UserProfileController.cs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) rename DrinkRateAPI/ApiModels/UserProfile/{ChangeAdminStatusBody.cs => UserProfileAdminStatusPut.cs} (70%) diff --git a/DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs b/DrinkRateAPI/ApiModels/UserProfile/UserProfileAdminStatusPut.cs similarity index 70% rename from DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs rename to DrinkRateAPI/ApiModels/UserProfile/UserProfileAdminStatusPut.cs index 238546f..b7dc8c7 100644 --- a/DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs +++ b/DrinkRateAPI/ApiModels/UserProfile/UserProfileAdminStatusPut.cs @@ -1,6 +1,6 @@ namespace DrinkRateAPI.ApiModels.UserProfile; -public class ChangeAdminStatusBody +public class UserProfileAdminStatusPut { public bool ChangeStatusTo { get; set; } } \ No newline at end of file diff --git a/DrinkRateAPI/Controllers/UserProfileController.cs b/DrinkRateAPI/Controllers/UserProfileController.cs index afc1bcf..a81aaf8 100644 --- a/DrinkRateAPI/Controllers/UserProfileController.cs +++ b/DrinkRateAPI/Controllers/UserProfileController.cs @@ -18,19 +18,18 @@ public class UserProfileController : ControllerBase _logger = logger; _userProfileService = userProfileService; } - - [HttpPut(Name = "user_profile")] - public UserProfileGet PutUserProfile(UserProfilePut userProfile) + + public UserProfileGet PutUserProfile([FromBody] UserProfilePut userProfile) { throw new ApplicationException(); var x = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; //HttpContext.User.Identities.First(); return new(); } - + [HttpPut("{userId}/adminStatus")] [Authorize(Policy = "AdminOnly")] [Produces("application/json")] - public async Task PutUserAdminStatus(string userId, [FromBody] ChangeAdminStatusBody body) + public async Task PutUserAdminStatus(string userId, [FromBody] UserProfileAdminStatusPut body) { var changedProfile = await _userProfileService.PutUserProfileAdminStatusAsync(userId, body.ChangeStatusTo); From ae723cecaf26f2cc403350d02189bf5f6121a030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vrabec?= Date: Mon, 11 Aug 2025 07:56:11 +0200 Subject: [PATCH 08/14] Add more exceptions --- .../Exceptions/ExceptionHandlingMiddleware.cs | 47 +++++++++++++++++++ DrinkRateAPI/Exceptions/Exceptions.cs | 35 +++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs diff --git a/DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs b/DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..e4e4740 --- /dev/null +++ b/DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs @@ -0,0 +1,47 @@ +using System.Net; + +namespace DrinkRateAPI.Exceptions; + +public record ExceptionResponse(HttpStatusCode StatusCode, string Description); + +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + _logger.LogError(exception, "An unexpected error occurred."); + + + ExceptionResponse response = exception switch + { + ApplicationException _ => new ExceptionResponse(HttpStatusCode.BadRequest, "Application exception occurred."), + KeyNotFoundException _ => new ExceptionResponse(HttpStatusCode.NotFound, "The request key not found."), + UnauthorizedAccessException _ => new ExceptionResponse(HttpStatusCode.Unauthorized, "Unauthorized."), + _ => new ExceptionResponse(HttpStatusCode.InternalServerError, "Internal server error. Please retry later.") + }; + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)response.StatusCode; + await context.Response.WriteAsJsonAsync(response); + } +} \ No newline at end of file diff --git a/DrinkRateAPI/Exceptions/Exceptions.cs b/DrinkRateAPI/Exceptions/Exceptions.cs index 14c8432..c005b85 100644 --- a/DrinkRateAPI/Exceptions/Exceptions.cs +++ b/DrinkRateAPI/Exceptions/Exceptions.cs @@ -1,7 +1,38 @@ namespace DrinkRateAPI.Exceptions; -public class DrinkRateException : Exception; +public abstract class DrinkRateException : Exception; +/// +/// 400 - Bad request +/// +public class BadRequestException : DrinkRateException; + +/// +/// 401 - Unauthenticated +/// +public class UnauthenticatedException : DrinkRateException; + +/// +/// 402 - Payment required +/// +public class PaymentRequiredException : DrinkRateException; + +/// +/// 403 - Forbidden +/// +public class ForbiddenException : DrinkRateException; + +/// +/// 404 - Not found +/// public class NotFoundException : DrinkRateException; -public class UnauthorizedException : DrinkRateException; \ No newline at end of file +/// +/// 418 - I'm a teapot +/// +public class IamATeapotException : DrinkRateException; + +/// +/// 451 - Unavailable for lagal reasons +/// +public class UnavailableForLagalReasonsException : DrinkRateException; \ No newline at end of file From 1906d0de0df4585784c6453271d0d7004c52d67b Mon Sep 17 00:00:00 2001 From: martinshoob Date: Mon, 11 Aug 2025 18:18:31 +0200 Subject: [PATCH 09/14] Remove explicit fail call in admin authorization --- .../AdminOnlyRequirement.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs b/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs index e972e3b..f850568 100644 --- a/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs +++ b/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs @@ -2,6 +2,7 @@ using DrinkRateAPI.DbEntities; using DrinkRateAPI.Services; namespace DrinkRateAPI.AuthorizationPolicies; + using Microsoft.AspNetCore.Authorization; public class AdminOnlyRequirement : IAuthorizationRequirement @@ -22,28 +23,14 @@ public class AdminOnlyHandler : AuthorizationHandler } protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, + AuthorizationHandlerContext context, AdminOnlyRequirement requirement) { - DbUserProfile userProfile; - - try - { - userProfile = await _applicationUserService.UserProfileByApplicationUserAsync(context.User); - } - catch (Exception _) - { - context.Fail(); - return; - } + var userProfile = await _applicationUserService.UserProfileByApplicationUserAsync(context.User); if (_userProfileService.IsUserProfileAdmin(userProfile)) { context.Succeed(requirement); } - else - { - context.Fail(); - } } } \ No newline at end of file From 7bf7f23925e396d98a4c94fba90e94ef1e40d177 Mon Sep 17 00:00:00 2001 From: martinshoob Date: Mon, 11 Aug 2025 18:36:12 +0200 Subject: [PATCH 10/14] Add exception handling middleware Implement global exception handling to provide consistent error responses. Registes custom exceptions with corresponding HTTP status codes and descriptions. --- .../Controllers/UserProfileController.cs | 1 + .../Exceptions/ExceptionHandlingMiddleware.cs | 19 ++++++++++++------- DrinkRateAPI/Program.cs | 3 +++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/DrinkRateAPI/Controllers/UserProfileController.cs b/DrinkRateAPI/Controllers/UserProfileController.cs index a81aaf8..7c4cfa0 100644 --- a/DrinkRateAPI/Controllers/UserProfileController.cs +++ b/DrinkRateAPI/Controllers/UserProfileController.cs @@ -19,6 +19,7 @@ public class UserProfileController : ControllerBase _userProfileService = userProfileService; } + [HttpPut] public UserProfileGet PutUserProfile([FromBody] UserProfilePut userProfile) { throw new ApplicationException(); diff --git a/DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs b/DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs index e4e4740..82fb158 100644 --- a/DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs +++ b/DrinkRateAPI/Exceptions/ExceptionHandlingMiddleware.cs @@ -1,8 +1,9 @@ using System.Net; +using Microsoft.AspNetCore.Http.HttpResults; namespace DrinkRateAPI.Exceptions; -public record ExceptionResponse(HttpStatusCode StatusCode, string Description); +public record ExceptionResponse(int StatusCode, string Description); public class ExceptionHandlingMiddleware { @@ -32,16 +33,20 @@ public class ExceptionHandlingMiddleware _logger.LogError(exception, "An unexpected error occurred."); - ExceptionResponse response = exception switch + var response = exception switch { - ApplicationException _ => new ExceptionResponse(HttpStatusCode.BadRequest, "Application exception occurred."), - KeyNotFoundException _ => new ExceptionResponse(HttpStatusCode.NotFound, "The request key not found."), - UnauthorizedAccessException _ => new ExceptionResponse(HttpStatusCode.Unauthorized, "Unauthorized."), - _ => new ExceptionResponse(HttpStatusCode.InternalServerError, "Internal server error. Please retry later.") + BadRequestException _ => new ExceptionResponse(StatusCodes.Status400BadRequest, "Application exception occurred."), + NotFoundException _ => new ExceptionResponse(StatusCodes.Status404NotFound, "The request key not found."), + UnauthenticatedException _ => new ExceptionResponse(StatusCodes.Status401Unauthorized, "Unauthorized."), + PaymentRequiredException _ => new ExceptionResponse(StatusCodes.Status402PaymentRequired, "Payment required."), + ForbiddenException _ => new ExceptionResponse(StatusCodes.Status403Forbidden, "Forbidden."), + IamATeapotException _ => new ExceptionResponse(StatusCodes.Status418ImATeapot, "I am a teapot."), + UnavailableForLagalReasonsException _ => new ExceptionResponse(StatusCodes.Status451UnavailableForLegalReasons, "Unavailable for legal reasons."), + _ => new ExceptionResponse(StatusCodes.Status500InternalServerError, "Internal server error. Please retry later.") }; context.Response.ContentType = "application/json"; - context.Response.StatusCode = (int)response.StatusCode; + context.Response.StatusCode = response.StatusCode; await context.Response.WriteAsJsonAsync(response); } } \ No newline at end of file diff --git a/DrinkRateAPI/Program.cs b/DrinkRateAPI/Program.cs index 051ce01..9da7e7d 100644 --- a/DrinkRateAPI/Program.cs +++ b/DrinkRateAPI/Program.cs @@ -1,6 +1,7 @@ using DrinkRateAPI.AuthorizationPolicies; using DrinkRateAPI.Contexts; using DrinkRateAPI.DbEntities; +using DrinkRateAPI.Exceptions; using DrinkRateAPI.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -70,6 +71,8 @@ if (app.Environment.IsDevelopment()) app.MapIdentityApi(); +app.UseMiddleware(); + app.UseHttpsRedirection(); app.UseAuthorization(); From 1dc37d328240804440015facbca4cfe6c7274a4d Mon Sep 17 00:00:00 2001 From: martinshoob Date: Mon, 11 Aug 2025 18:46:35 +0200 Subject: [PATCH 11/14] Handle not found user profile gracefully Changes from throwing an exception when a user profile is not found to returning null. This prevents the application from crashing when a user profile is not found, allowing for more graceful error handling. --- DrinkRateAPI/Services/ApplicationUserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DrinkRateAPI/Services/ApplicationUserService.cs b/DrinkRateAPI/Services/ApplicationUserService.cs index a2f735d..7df4ac0 100644 --- a/DrinkRateAPI/Services/ApplicationUserService.cs +++ b/DrinkRateAPI/Services/ApplicationUserService.cs @@ -13,7 +13,7 @@ public class ApplicationUserService(ApplicationDbContext context) { var appUserId = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value; var profile = await _context.UserProfiles - .FirstAsync(x => x.ApplicationUserId.ToString() == appUserId) + .FirstOrDefaultAsync(x => x.ApplicationUserId.ToString() == appUserId) ?? throw new NotFoundException(); return profile; From 4216535016dbcc23d6033ad041a91f45099c0f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vrabec?= Date: Mon, 11 Aug 2025 19:33:55 +0200 Subject: [PATCH 12/14] Prevent deleted user authentication --- DrinkRateAPI/Services/ApplicationUserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DrinkRateAPI/Services/ApplicationUserService.cs b/DrinkRateAPI/Services/ApplicationUserService.cs index a2f735d..c30c395 100644 --- a/DrinkRateAPI/Services/ApplicationUserService.cs +++ b/DrinkRateAPI/Services/ApplicationUserService.cs @@ -13,7 +13,7 @@ public class ApplicationUserService(ApplicationDbContext context) { var appUserId = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value; var profile = await _context.UserProfiles - .FirstAsync(x => x.ApplicationUserId.ToString() == appUserId) + .FirstAsync(x => x.ApplicationUserId.ToString() == appUserId && !x.IsDeleted) ?? throw new NotFoundException(); return profile; From 84e16d655d8008070480e6c7e9c4bd5d0c9e8fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vrabec?= Date: Mon, 11 Aug 2025 20:07:31 +0200 Subject: [PATCH 13/14] Add exception messages --- DrinkRateAPI/Exceptions/Exceptions.cs | 48 ++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/DrinkRateAPI/Exceptions/Exceptions.cs b/DrinkRateAPI/Exceptions/Exceptions.cs index c005b85..0a76e7e 100644 --- a/DrinkRateAPI/Exceptions/Exceptions.cs +++ b/DrinkRateAPI/Exceptions/Exceptions.cs @@ -1,38 +1,70 @@ namespace DrinkRateAPI.Exceptions; -public abstract class DrinkRateException : Exception; +public class DrinkRateException : Exception +{ + public DrinkRateException() : base() { } + public DrinkRateException(string message) : base(message) { } +} /// /// 400 - Bad request /// -public class BadRequestException : DrinkRateException; +public class BadRequestException : DrinkRateException +{ + public BadRequestException() : base() { } + public BadRequestException(string message) : base(message) { } +} /// /// 401 - Unauthenticated /// -public class UnauthenticatedException : DrinkRateException; +public class UnauthenticatedException : DrinkRateException +{ + public UnauthenticatedException() : base() { } + public UnauthenticatedException(string message) : base(message) { } +} /// /// 402 - Payment required /// -public class PaymentRequiredException : DrinkRateException; +public class PaymentRequiredException : DrinkRateException +{ + public PaymentRequiredException() : base() { } + public PaymentRequiredException(string message) : base(message) { } +} /// /// 403 - Forbidden /// -public class ForbiddenException : DrinkRateException; +public class ForbiddenException : DrinkRateException +{ + public ForbiddenException() : base() { } + public ForbiddenException(string message) : base(message) { } +} /// /// 404 - Not found /// -public class NotFoundException : DrinkRateException; +public class NotFoundException : DrinkRateException +{ + public NotFoundException() : base() { } + public NotFoundException(string message) : base(message) { } +} /// /// 418 - I'm a teapot /// -public class IamATeapotException : DrinkRateException; +public class IamATeapotException : DrinkRateException +{ + public IamATeapotException() : base() { } + public IamATeapotException(string message) : base(message) { } +} /// /// 451 - Unavailable for lagal reasons /// -public class UnavailableForLagalReasonsException : DrinkRateException; \ No newline at end of file +public class UnavailableForLagalReasonsException : DrinkRateException +{ + public UnavailableForLagalReasonsException() : base() { } + public UnavailableForLagalReasonsException(string message) : base(message) { } +} \ No newline at end of file From 602496ec08eb6333debd0fee95d2c1089f8a7229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vrabec?= Date: Mon, 11 Aug 2025 22:08:13 +0200 Subject: [PATCH 14/14] Add user profile endpoints --- .../UserProfile/UserProfileAdminStatusPut.cs | 6 - .../ApiModels/UserProfile/UserProfileGet.cs | 23 ++++ .../ApiModels/UserProfile/UserProfilePut.cs | 7 +- .../UserProfile/UserProfileSelfPut.cs | 9 ++ .../Controllers/UserProfileController.cs | 33 ++++-- DrinkRateAPI/DbEntities/DbUserProfile.cs | 1 - DrinkRateAPI/Services/UserProfileService.cs | 109 ++++++++++++++---- 7 files changed, 147 insertions(+), 41 deletions(-) delete mode 100644 DrinkRateAPI/ApiModels/UserProfile/UserProfileAdminStatusPut.cs create mode 100644 DrinkRateAPI/ApiModels/UserProfile/UserProfileSelfPut.cs diff --git a/DrinkRateAPI/ApiModels/UserProfile/UserProfileAdminStatusPut.cs b/DrinkRateAPI/ApiModels/UserProfile/UserProfileAdminStatusPut.cs deleted file mode 100644 index b7dc8c7..0000000 --- a/DrinkRateAPI/ApiModels/UserProfile/UserProfileAdminStatusPut.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DrinkRateAPI.ApiModels.UserProfile; - -public class UserProfileAdminStatusPut -{ - public bool ChangeStatusTo { get; set; } -} \ No newline at end of file diff --git a/DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs b/DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs index ed241cf..5f21fec 100644 --- a/DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs +++ b/DrinkRateAPI/ApiModels/UserProfile/UserProfileGet.cs @@ -2,5 +2,28 @@ namespace DrinkRateAPI.ApiModels.UserProfile; public class UserProfileGet { + /// + /// User profile ID + /// + public string Id { get; set; } + /// + /// User profile name + /// + public string UserName { get; set; } + + /// + /// Is user admin + /// + public bool IsAdmin { get; set; } + + /// + /// Is user deleted + /// + public bool IsDeleted { get; set; } + + /// + /// Applicaton user ID of the user profile + /// + public string ApplicationUserId { get; set; } } \ No newline at end of file diff --git a/DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs b/DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs index 020071d..ecf2fd4 100644 --- a/DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs +++ b/DrinkRateAPI/ApiModels/UserProfile/UserProfilePut.cs @@ -1,6 +1,9 @@ namespace DrinkRateAPI.ApiModels.UserProfile; -public class UserProfilePut +public class UserProfilePut : UserProfileSelfPut { - public string UserName { get; set; } + /// + /// Is user admin + /// + public bool? IsAdmin { get; set; } } \ No newline at end of file diff --git a/DrinkRateAPI/ApiModels/UserProfile/UserProfileSelfPut.cs b/DrinkRateAPI/ApiModels/UserProfile/UserProfileSelfPut.cs new file mode 100644 index 0000000..8ca7d1b --- /dev/null +++ b/DrinkRateAPI/ApiModels/UserProfile/UserProfileSelfPut.cs @@ -0,0 +1,9 @@ +namespace DrinkRateAPI.ApiModels.UserProfile; + +public class UserProfileSelfPut +{ + /// + /// User profile name + /// + public string? UserName { get; set; } +} \ No newline at end of file diff --git a/DrinkRateAPI/Controllers/UserProfileController.cs b/DrinkRateAPI/Controllers/UserProfileController.cs index 7c4cfa0..eb527aa 100644 --- a/DrinkRateAPI/Controllers/UserProfileController.cs +++ b/DrinkRateAPI/Controllers/UserProfileController.cs @@ -1,4 +1,3 @@ -using System.Security.Claims; using DrinkRateAPI.ApiModels.UserProfile; using DrinkRateAPI.Services; using Microsoft.AspNetCore.Authorization; @@ -10,30 +9,40 @@ namespace DrinkRateAPI.Controllers; [Route("userProfile")] public class UserProfileController : ControllerBase { - private readonly ILogger _logger; private readonly UserProfileService _userProfileService; - public UserProfileController(ILogger logger, UserProfileService userProfileService) + public UserProfileController(UserProfileService userProfileService) { - _logger = logger; _userProfileService = userProfileService; } [HttpPut] - public UserProfileGet PutUserProfile([FromBody] UserProfilePut userProfile) + [Produces("application/json")] + public async Task PutUserProfileSelf([FromBody] UserProfileSelfPut userProfile) { - throw new ApplicationException(); - var x = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; //HttpContext.User.Identities.First(); - return new(); + return await _userProfileService.PutUserProfileSelfAsync(User, userProfile); } - [HttpPut("{userId}/adminStatus")] + [HttpGet] + [Produces("application/json")] + public async Task GetUserProfileSelf() + { + return await _userProfileService.GetUserProfileSelfAsync(User); + } + + [HttpPut("{userId}")] [Authorize(Policy = "AdminOnly")] [Produces("application/json")] - public async Task PutUserAdminStatus(string userId, [FromBody] UserProfileAdminStatusPut body) + public async Task PutUserProfile(string userId, [FromBody] UserProfilePut userProfile) { - var changedProfile = await _userProfileService.PutUserProfileAdminStatusAsync(userId, body.ChangeStatusTo); + return await _userProfileService.PutUserProfileAsync(User, userProfile, userId); + } - return Ok(changedProfile); + [HttpGet("{userId}")] + [Authorize(Policy = "AdminOnly")] + [Produces("application/json")] + public async Task GetUserProfile(string userId) + { + return await _userProfileService.GetUserProfileAsync(User, userId); } } \ No newline at end of file diff --git a/DrinkRateAPI/DbEntities/DbUserProfile.cs b/DrinkRateAPI/DbEntities/DbUserProfile.cs index bde7fa5..55fa050 100644 --- a/DrinkRateAPI/DbEntities/DbUserProfile.cs +++ b/DrinkRateAPI/DbEntities/DbUserProfile.cs @@ -20,6 +20,5 @@ public class DbUserProfile : DbEntityWithHistory public bool IsDeleted { get; set; } public Guid ApplicationUserId { get; set; } - public virtual DbApplicationUser ApplicationUser { get; set; } } \ No newline at end of file diff --git a/DrinkRateAPI/Services/UserProfileService.cs b/DrinkRateAPI/Services/UserProfileService.cs index 8726f93..ad8c03d 100644 --- a/DrinkRateAPI/Services/UserProfileService.cs +++ b/DrinkRateAPI/Services/UserProfileService.cs @@ -2,12 +2,8 @@ using System.Security.Claims; using DrinkRateAPI.ApiModels.UserProfile; using DrinkRateAPI.Contexts; using DrinkRateAPI.DbEntities; -using DrinkRateAPI.DbEntities; using DrinkRateAPI.Exceptions; using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - namespace DrinkRateAPI.Services; @@ -21,29 +17,102 @@ public class UserProfileService(ApplicationDbContext context, ApplicationUserSer return userProfile.IsAdmin; } - public async Task PutUserProfileAdminStatusAsync(string userId, bool changeStatusTo) + public async Task PutUserProfileSelfAsync(ClaimsPrincipal identity, UserProfileSelfPut userProfileSelfPut) { - var userProfile = GetUserProfileById(userId); - userProfile.IsAdmin = changeStatusTo; + var authenticatedUser = await _applicationUserService.UserProfileByApplicationUserAsync(identity); + + var userId = authenticatedUser.Id.ToString(); + await PutUserProfile(userProfileSelfPut, userId, false); + + return await GetUserProfile(userId); + } + + public async Task GetUserProfileSelfAsync(ClaimsPrincipal identity) + { + var authenticatedUser = await _applicationUserService.UserProfileByApplicationUserAsync(identity); + + var userId = authenticatedUser.Id.ToString(); + + return await GetUserProfile(userId); + } + + public async Task PutUserProfileAsync(ClaimsPrincipal identity, UserProfilePut userProfilePut, string userId) + { + var authenticatedUser = await _applicationUserService.UserProfileByApplicationUserAsync(identity); + + if (authenticatedUser.Id.ToString() == userId) + { + // Prevent admin de-admining him/herself + await PutUserProfile(userProfilePut, userId, false); + } + else + { + await PutUserProfile(userProfilePut, userId, IsUserProfileAdmin(authenticatedUser)); + } + + return await GetUserProfile(userId); + } + + public async Task GetUserProfileAsync(ClaimsPrincipal identity, string userId) + { + var authenticatedUser = await _applicationUserService.UserProfileByApplicationUserAsync(identity); + + return await GetUserProfile(userId); + } + + private async Task PutUserProfile(TUserProfilePut userProfilePut, string userId, bool byAdmin) where TUserProfilePut : UserProfileSelfPut + { + var userProfile = await GetUserProfileById(userId); + + if (!string.IsNullOrEmpty(userProfilePut.UserName) && userProfile.UserName != userProfilePut.UserName) + { + var userByName = await TryGetUserProfileByUserName(userProfilePut.UserName); + if (userByName == null) + { + userProfile.UserName = userProfilePut.UserName; + } + else + { + throw new BadRequestException($"User with username {userProfilePut.UserName} already exists"); + } + } + + if (byAdmin && userProfilePut is UserProfilePut adminPut && adminPut.IsAdmin != null) + { + userProfile.IsAdmin = (bool)adminPut.IsAdmin; + } + _context.UserProfiles.Update(userProfile); await _context.SaveChangesAsync(); - - return userProfile; - } - - public async Task PutUserProfileAsync(UserProfilePut userProfile, ClaimsPrincipal identity) - { - var profile = _applicationUserService.UserProfileByApplicationUserAsync(identity); - - - - return new(); } - public DbUserProfile GetUserProfileById(string userId) + private async Task GetUserProfile(string userId) { - var userProfile = _context.UserProfiles.FirstOrDefault(x => x.Id.ToString() == userId); + var userProfile = await GetUserProfileById(userId); + + var userProfileGet = new UserProfileGet + { + Id = userProfile.Id.ToString(), + UserName = userProfile.UserName, + IsAdmin = userProfile.IsAdmin, + IsDeleted = userProfile.IsDeleted, + ApplicationUserId = userProfile.ApplicationUserId.ToString(), + }; + + return userProfileGet; + } + + private async Task GetUserProfileById(string userId) + { + var userProfile = await _context.UserProfiles.FirstOrDefaultAsync(x => x.Id.ToString() == userId); return userProfile ?? throw new NotFoundException(); } + + private async Task TryGetUserProfileByUserName(string userName) + { + var userProfile = await _context.UserProfiles.FirstOrDefaultAsync(x => x.UserName == userName); + + return userProfile; + } } \ No newline at end of file