From c56618ba7aca4f15b4b020fb3a69d73a26d971dd Mon Sep 17 00:00:00 2001 From: raczu Date: Sun, 2 Jun 2024 17:23:58 +0200 Subject: [PATCH] feat: add placeholders for endpoints --- .../ServiceExceptionHandlerTests.cs | 23 +++ .../Authentication/AuthServiceTests.cs | 2 +- .../ReasnAPI/Common/IAssemblyMarker.cs | 2 +- .../ReasnAPI/Controllers/EventsController.cs | 139 ++++++++++++++++++ .../ReasnAPI/Controllers/MeController.cs | 104 +++++++++++++ .../ReasnAPI/Controllers/UsersController.cs | 32 ++++ .../Exceptions/ServiceExceptionHandler.cs | 6 + Server/ReasnAPI/ReasnAPI/Program.cs | 5 +- .../ReasnAPI/ReasnAPI/Services/UserService.cs | 40 +++++ Server/ReasnAPI/ReasnAPI/appsettings.json | 7 +- 10 files changed, 355 insertions(+), 5 deletions(-) create mode 100644 Server/ReasnAPI/ReasnAPI/Controllers/EventsController.cs create mode 100644 Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs create mode 100644 Server/ReasnAPI/ReasnAPI/Services/UserService.cs diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ServiceExceptionHandlerTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ServiceExceptionHandlerTests.cs index a76f1f12..87d74d60 100644 --- a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ServiceExceptionHandlerTests.cs +++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Exceptions/ServiceExceptionHandlerTests.cs @@ -91,4 +91,27 @@ public async Task HandleException_WhenVerificationException_ShouldReturnProblemD Assert.AreEqual(exception, problemDetailsContext.Exception); Assert.AreEqual(exception.Message, problemDetailsContext.ProblemDetails.Detail); } + + [TestMethod] + public async Task HandleException_WhenUnauthorizedAccessException_ShouldReturnProblemDetails() + { + var httpContext = new DefaultHttpContext(); + var exception = new UnauthorizedAccessException("Unauthorized access"); + + ProblemDetailsContext? problemDetailsContext = null; + _mockProblemDetailsService.Setup(x => + x.TryWriteAsync(It.IsAny())) + .Callback(context => problemDetailsContext = context) + .ReturnsAsync(true); + + await _handler.TryHandleAsync(httpContext, exception, CancellationToken.None); + + Assert.AreEqual((int)HttpStatusCode.Unauthorized, httpContext.Response.StatusCode); + Assert.IsNotNull(problemDetailsContext); + Assert.AreEqual("https://datatracker.ietf.org/doc/html/rfc7235#section-3.1", + problemDetailsContext.ProblemDetails.Type); + Assert.AreEqual("Unauthorized", problemDetailsContext.ProblemDetails.Title); + Assert.AreEqual(exception, problemDetailsContext.Exception); + Assert.AreEqual(exception.Message, problemDetailsContext.ProblemDetails.Detail); + } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs index c593feb2..f0b63cb5 100644 --- a/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs +++ b/Server/ReasnAPI/ReasnAPI.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs @@ -105,7 +105,7 @@ public void Register_WhenUserWithPhoneAlreadyExists_ShouldThrowBadRequestExcepti Username = "jstark", Phone = "+123 456789" }; - + Assert.ThrowsException(() => _service.Register(request)); } diff --git a/Server/ReasnAPI/ReasnAPI/Common/IAssemblyMarker.cs b/Server/ReasnAPI/ReasnAPI/Common/IAssemblyMarker.cs index 916dc1d9..0e8dae06 100644 --- a/Server/ReasnAPI/ReasnAPI/Common/IAssemblyMarker.cs +++ b/Server/ReasnAPI/ReasnAPI/Common/IAssemblyMarker.cs @@ -1,4 +1,4 @@ -namespace ReasnAPI.Validators; +namespace ReasnAPI.Common; public interface IAssemblyMarker { diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/EventsController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/EventsController.cs new file mode 100644 index 00000000..1c116b9c --- /dev/null +++ b/Server/ReasnAPI/ReasnAPI/Controllers/EventsController.cs @@ -0,0 +1,139 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ReasnAPI.Models.Database; + +namespace ReasnAPI.Controllers; + +[ApiController] +[Route("[controller]")] +public class EventsController : ControllerBase +{ + private readonly ReasnContext _context; + + public EventsController(ReasnContext context) + { + _context = context; + } + + [HttpGet] + public IActionResult GetEvents() + { + throw new NotImplementedException(); + } + + [HttpPost] + [Authorize(Roles = "Admin, Organizer")] + public IActionResult CreateEvent() + { + throw new NotImplementedException(); + } + + [HttpGet] + [Route("{slug}")] + public IActionResult GetEventBySlug(string slug) + { + throw new NotImplementedException(); + } + + [HttpPut] + [Authorize(Roles = "Admin, Organizer")] + [Route("{slug}")] + public IActionResult UpdateEvent(string slug) + { + throw new NotImplementedException(); + } + + [HttpGet] + [Authorize(Roles = "Admin")] + [Route("requests")] + public IActionResult GetEventsRequests(string slug) + { + throw new NotImplementedException(); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + [Route("{slug}/approve")] + public IActionResult ApproveEventRequest(string slug) + { + throw new NotImplementedException(); + } + + [HttpPost] + [Authorize(Roles = "Admin, Organizer")] + [Route("{slug}/images")] + public IActionResult AddEventImage(string slug) + { + throw new NotImplementedException(); + } + + [HttpPut] + [Authorize(Roles = "Admin, Organizer")] + [Route("{slug}/images/{imageId:int}")] + public IActionResult UpdateEventImage(string slug, int imageId) + { + throw new NotImplementedException(); + } + + [HttpDelete] + [Authorize(Roles = "Admin, Organizer")] + [Route("{slug}/images/{imageId:int}")] + public IActionResult DeleteEventImage(string slug, int imageId) + { + throw new NotImplementedException(); + } + + [HttpGet] + [Route("{slug}/users")] + public IActionResult GetEventUsers(string slug) + { + throw new NotImplementedException(); + } + + [HttpGet] + [Route("{slug}/comments")] + public IActionResult GetEventComments(string slug) + { + throw new NotImplementedException(); + } + + [HttpPost] + [Authorize] + [Route("{slug}/comments")] + public IActionResult AddEventComment(string slug) + { + throw new NotImplementedException(); + } + + [HttpPost] + [Authorize(Roles = "Admin, Organizer")] + [Route("{slug}/parameters")] + public IActionResult AddEventParameter(string slug) + { + throw new NotImplementedException(); + } + + [HttpDelete] + [Authorize(Roles = "Admin, Organizer")] + [Route("{slug}/parameters/{parameterId:int}")] + public IActionResult DeleteEventParameter(string slug, int parameterId) + { + throw new NotImplementedException(); + } + + [HttpGet] + [Authorize(Roles = "Admin, Organizer")] + [Route("parameters")] + public IActionResult GetEventsParameters(string slug) + { + throw new NotImplementedException(); + } + + [HttpDelete] + [Authorize(Roles = "Admin")] + [Route("parameters/{parameterId:int}")] + public IActionResult DeleteEventsParameter(int parameterId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs new file mode 100644 index 00000000..825b1860 --- /dev/null +++ b/Server/ReasnAPI/ReasnAPI/Controllers/MeController.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ReasnAPI.Mappers; +using ReasnAPI.Models.DTOs; +using ReasnAPI.Services; + +namespace ReasnAPI.Controllers; + +[ApiController] +[Authorize] +[Route("[controller]")] +public class MeController : ControllerBase +{ + private readonly UserService _userService; + + public MeController(UserService userService) + { + _userService = userService; + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult GetCurrentUser() + { + var user = _userService.GetCurrentUser(); + return Ok(user.ToDto()); + } + + [HttpPut] + public IActionResult UpdateCurrentUser() + { + throw new NotImplementedException(); + } + + [HttpPost] + [Route("image")] + public IActionResult AddCurrentUserImage() + { + throw new NotImplementedException(); + } + + [HttpPut] + [Route("image")] + public IActionResult UpdateCurrentUserImage() + { + throw new NotImplementedException(); + } + + [HttpDelete] + [Route("image")] + public IActionResult DeleteCurrentUserImage() + { + throw new NotImplementedException(); + } + + [HttpGet] + [Route("interests")] + public IActionResult GetCurrentUserInterests() + { + throw new NotImplementedException(); + } + + [HttpPost] + [Route("interests")] + public IActionResult AddCurrentUserInterest() + { + throw new NotImplementedException(); + } + + [HttpDelete] + [Route("interests/{interestId:int}")] + public IActionResult DeleteCurrentUserInterest(int interestId) + { + throw new NotImplementedException(); + } + + [HttpGet] + [Route("events")] + public IActionResult GetCurrentUserEvents() + { + throw new NotImplementedException(); + } + + [HttpPost] + [Route("events/{slug}/enroll")] + public IActionResult EnrollCurrentUserInEvent(string slug) + { + throw new NotImplementedException(); + } + + [HttpPost] + [Route("events/{slug}/confirm")] + public IActionResult ConfirmCurrentUserEventAttendance(string slug) + { + throw new NotImplementedException(); + } + + [HttpGet] + [Route("events/recommendations")] + public IActionResult GetCurrentUserEventRecommendations() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs index ca021725..cb2849ab 100644 --- a/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs +++ b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace ReasnAPI.Controllers; @@ -6,10 +7,41 @@ namespace ReasnAPI.Controllers; [Route("[controller]")] public class UsersController : ControllerBase { + [HttpGet] + [Authorize(Roles = "Admin")] + public IActionResult GetUsers() + { + throw new NotImplementedException(); + } + [HttpGet] [Route("{username}")] public IActionResult GetUserByUsername(string username) { throw new NotImplementedException(); } + + [HttpPut] + [Authorize] + [Route("{username}")] + public IActionResult UpdateUser(string username) + { + throw new NotImplementedException(); + } + + [HttpGet] + [Authorize] + [Route("interests")] + public IActionResult GetUsersInterests(string username) + { + throw new NotImplementedException(); + } + + [HttpDelete] + [Authorize(Roles = "Admin")] + [Route("interests/{interestId:int}")] + public IActionResult DeleteUserInterest(int interestId) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Exceptions/ServiceExceptionHandler.cs b/Server/ReasnAPI/ReasnAPI/Exceptions/ServiceExceptionHandler.cs index fd76c213..bdd4a3c1 100644 --- a/Server/ReasnAPI/ReasnAPI/Exceptions/ServiceExceptionHandler.cs +++ b/Server/ReasnAPI/ReasnAPI/Exceptions/ServiceExceptionHandler.cs @@ -44,6 +44,12 @@ public async ValueTask TryHandleAsync( problemDetails.Title = "A verification error occurred"; break; + case UnauthorizedAccessException: + httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1"; + problemDetails.Title = "Unauthorized"; + break; + default: return false; } diff --git a/Server/ReasnAPI/ReasnAPI/Program.cs b/Server/ReasnAPI/ReasnAPI/Program.cs index 574a07ab..5c7c777f 100644 --- a/Server/ReasnAPI/ReasnAPI/Program.cs +++ b/Server/ReasnAPI/ReasnAPI/Program.cs @@ -8,11 +8,12 @@ using FluentValidation; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; +using ReasnAPI.Common; using ReasnAPI.Exceptions; using ReasnAPI.Middlewares; using ReasnAPI.Models.Database; +using ReasnAPI.Services; using ReasnAPI.Services.Authentication; -using ReasnAPI.Validators; var builder = WebApplication.CreateSlimBuilder(args); var config = builder.Configuration; @@ -50,9 +51,11 @@ { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); +builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddValidatorsFromAssemblyContaining(); var dataSourceBuilder = new NpgsqlDataSourceBuilder(config.GetConnectionString("DefaultValue")); diff --git a/Server/ReasnAPI/ReasnAPI/Services/UserService.cs b/Server/ReasnAPI/ReasnAPI/Services/UserService.cs new file mode 100644 index 00000000..3c08f9af --- /dev/null +++ b/Server/ReasnAPI/ReasnAPI/Services/UserService.cs @@ -0,0 +1,40 @@ +using System.Security.Claims; +using ReasnAPI.Models.Database; +using ReasnAPI.Services.Exceptions; + +namespace ReasnAPI.Services; + +public class UserService +{ + private readonly ReasnContext _context; + private readonly IHttpContextAccessor _httpContextAccessor; + + public UserService(ReasnContext context, IHttpContextAccessor httpContextAccessor) + { + _context = context; + _httpContextAccessor = httpContextAccessor; + } + + public User GetCurrentUser() + { + var context = _httpContextAccessor.HttpContext; + if (context == null) + { + throw new InvalidOperationException("No HTTP context available"); + } + + var email = context.User.FindFirstValue(ClaimTypes.Email); + if (string.IsNullOrEmpty(email)) + { + throw new UnauthorizedAccessException("No email claim found in token"); + } + + var user = _context.Users.FirstOrDefault(u => u.Email == email); + if (user is null) + { + throw new NotFoundException("User associated with email not found"); + } + + return user; + } +} \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/appsettings.json b/Server/ReasnAPI/ReasnAPI/appsettings.json index 58588f6e..09ee7d87 100644 --- a/Server/ReasnAPI/ReasnAPI/appsettings.json +++ b/Server/ReasnAPI/ReasnAPI/appsettings.json @@ -4,11 +4,14 @@ "Serilog.Sinks.Console" ], "WriteTo": [ - { "Name": "Console" } + { + "Name": "Console" + } ] }, "ConnectionStrings": { - "DefaultValue": "Server=postgres;Port=5432;Database=reasn;User Id=dba;Password=sql;" + "DefaultValue": "Server=postgres;Port=5432;Database=reasn;User Id=dba;Password=sql;", + "LocalConnection": "Server=localhost;Port=5432;Database=reasn;User Id=dba;Password=sql;" }, "JwtSettings": { "Issuer": "http://localhost:5272",