diff --git a/.env b/.env index 673e590786e96e46a0ea58e0f4a06002a86fd218..c15e04b3edaa6bb59572efc0d419059f9fc6241a 100644 --- a/.env +++ b/.env @@ -4,10 +4,16 @@ MYSQL_IMAGE_TAG=8.0 # Database configuration -DB_PORT=3307 -DB_NAME=myDB -DB_USER=user -DB_PASSWORD=user +#DB_PORT=3307 +#DB_NAME=myDB +#DB_USER=user +#DB_PASSWORD=user +#DB_CHARSET=utf8mb4 + +DB_PORT=3308 +DB_NAME=AspNetCoreDb +DB_USER=root +DB_PASSWORD= DB_CHARSET=utf8mb4 # Service ports diff --git a/BookingMicroservice/Controllers/BookingController.cs b/BookingMicroservice/Controllers/BookingController.cs index 2f43c11993c21fa2f169c74583a39e2e95509f44..de800b69a4e0fbdae5b598ed496a012cddd9d6ed 100644 --- a/BookingMicroservice/Controllers/BookingController.cs +++ b/BookingMicroservice/Controllers/BookingController.cs @@ -1,6 +1,7 @@ using BookingMicroservice.Exceptions; using BookingMicroservice.Models; using BookingMicroservice.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; @@ -19,6 +20,7 @@ namespace BookingMicroservice.Controllers this.bookingService = bookingService; } + [Authorize] [HttpGet()] public IActionResult GetBookings(int? flightId = null, int? userId = null, BookingClass? bookingClass = null) { @@ -29,6 +31,7 @@ namespace BookingMicroservice.Controllers return Ok(bookings); } + [Authorize] [HttpGet("{id}")] public IActionResult GetBooking([FromRoute] int id) { @@ -39,6 +42,7 @@ namespace BookingMicroservice.Controllers return Ok(booking); } + [Authorize] [HttpPost()] public async Task<IActionResult> MakeBooking([FromBody] BookingCreation bookingCreationModel) { @@ -47,20 +51,32 @@ namespace BookingMicroservice.Controllers if (!int.TryParse(userIdValue, out int userId)) return BadRequest("Unable to get User Id from Token"); - Booking booking = await reservationComplianceService.TryCreateBookingAsync(bookingCreationModel.FlightId, userId, bookingCreationModel.BookingClass, bookingCreationModel.SeatId); - if (booking == null) - return BadRequest("Error in creating booking"); + try + { + Booking? booking = await reservationComplianceService.TryCreateBookingAsync(bookingCreationModel.FlightId, userId, bookingCreationModel.BookingClass, bookingCreationModel.SeatId); + if (booking == null) + return BadRequest("Error in creating booking"); - return Ok(booking); + return Ok(booking); + } + catch (InvalidOperationException exception) + { + return BadRequest(exception.Message); + } + catch (BookingException exception) + { + return BadRequest(exception.Message); + } } + [Authorize] [HttpPut("{bookingId}")] public async Task<IActionResult> UpdatedBookingSeat([FromRoute] int bookingId, [FromBody] BookingUpdate bookingUpdateModel) { try { await reservationComplianceService.TryBookSeatAsync(bookingId, bookingUpdateModel.SeatId); - return Ok(); + return Ok(new { BookedSeat = true, Message = "Seat booked successfully." }); } catch (InvalidOperationException exception) { @@ -68,7 +84,7 @@ namespace BookingMicroservice.Controllers } catch (BookingException exception) { - return BadRequest(exception.Message); + return Ok(new { BookedSeat = false, Message = exception.Message }); } } } diff --git a/BookingMicroservice/Handlers/RequestCookieHandler.cs b/BookingMicroservice/Handlers/RequestCookieHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..7ac6698ceac2ff4e74e8cd2bcbe787be93f67289 --- /dev/null +++ b/BookingMicroservice/Handlers/RequestCookieHandler.cs @@ -0,0 +1,37 @@ +using System.Net; + +namespace BookingMicroservice.Handlers +{ + public class RequestCookieHandler : DelegatingHandler + { + private readonly IHttpContextAccessor httpContextAccessor; + + public RequestCookieHandler(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpContext? context = httpContextAccessor.HttpContext; + + if (context?.Request.Cookies != null && request.RequestUri != null) + { + CookieContainer cookieContainer = new CookieContainer(); + foreach (KeyValuePair<string, string> cookie in context.Request.Cookies) + { + if (!string.IsNullOrEmpty(cookie.Value) && (cookie.Key == "AccessToken" || cookie.Key == "RefreshToken")) + cookieContainer.Add(request.RequestUri, new Cookie(cookie.Key, cookie.Value)); + } + + var cookieHeader = cookieContainer.GetCookieHeader(request.RequestUri); + if (!string.IsNullOrEmpty(cookieHeader)) + request.Headers.Add("Cookie", cookieHeader); + } + + return await base.SendAsync(request, cancellationToken); + } + + + } +} diff --git a/BookingMicroservice/Program.cs b/BookingMicroservice/Program.cs index c10b8dcfa3d98b6df96eedefff2344c3092c88a4..74cabbd729d2e59148649da179c50c918bf9dc0d 100644 --- a/BookingMicroservice/Program.cs +++ b/BookingMicroservice/Program.cs @@ -9,6 +9,8 @@ using System.Text; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddTransient<RequestCookieHandler>(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -29,14 +31,31 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) ValidIssuer = builder.Configuration["Jwt:Issuer"] ?? throw new InvalidOperationException("JWT Issuer is not configured."), ValidAudience = builder.Configuration["Jwt:Audience"] ?? throw new InvalidOperationException("JWT Audience is not configured.") }; + + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + Console.WriteLine("Authentication failed: " + context.Exception.Message); + return Task.CompletedTask; + }, + OnTokenValidated = context => + { + Console.WriteLine("Token validated"); + return Task.CompletedTask; + } + }; }); builder.Services.AddHttpClient<IFlightServiceClient, FlightServiceClient>(client => { - string baseURL = builder.Configuration["FlightMicroservice:BaseUrl"] ?? throw new InvalidOperationException("FlightMicroservice BaseUrl is not configured."); + string? baseURL = builder.Configuration["FlightMicroservice:BaseUrl"]; + if (string.IsNullOrEmpty(baseURL)) + throw new InvalidOperationException("FlightMicroservice BaseUrl is not configured."); + baseURL = baseURL.EndsWith("/") ? baseURL : baseURL + "/"; client.BaseAddress = new Uri(baseURL); -}); +}).AddHttpMessageHandler<RequestCookieHandler>(); ; builder.Services.AddScoped<IBookingService, BookingService>(); builder.Services.AddScoped<IReservationComplianceService, ReservationComplianceService>(); @@ -64,7 +83,8 @@ app.Use(async (context, next) => await next(); }); - +app.UseAuthentication(); app.UseAuthorization(); + app.MapControllers(); app.Run(); diff --git a/BookingMicroservice/Services/BookingService.cs b/BookingMicroservice/Services/BookingService.cs index d5efd2941e115d3ff46b3abe4c2a617ae3c9a8db..4c7a4fd983614f290d28e1ad3ee52bfaaf63d842 100644 --- a/BookingMicroservice/Services/BookingService.cs +++ b/BookingMicroservice/Services/BookingService.cs @@ -30,9 +30,9 @@ namespace BookingMicroservice.Services return bookings; } - public Booking CreateBooking(int flightId, int userId, BookingClass bookingClass, int? seatId) + public Booking CreateBooking(int flightId, int userId, BookingClass bookingClass) { - Booking booking = new Booking(flightId, userId, bookingClass, seatId); + Booking booking = new Booking(flightId, userId, bookingClass, null); dbContext.Bookings.Add(booking); dbContext.SaveChanges(); diff --git a/BookingMicroservice/Services/IBookingService.cs b/BookingMicroservice/Services/IBookingService.cs index d679c2eff5ed9fe8cc1d85c282800b013a0e9d8d..b5bd32727c612296a115e6323e204eac9741dd4c 100644 --- a/BookingMicroservice/Services/IBookingService.cs +++ b/BookingMicroservice/Services/IBookingService.cs @@ -4,7 +4,7 @@ namespace BookingMicroservice.Services { public interface IBookingService { - Booking CreateBooking(int flightId, int userId, BookingClass bookingClass, int? seatId); + Booking CreateBooking(int flightId, int userId, BookingClass bookingClass); List<Booking> GetBookings(int? flightId = null, int? userId = null, BookingClass? bookingClass = null); Booking? GetBooking(int bookingId); void DeleteBooking(int bookingId); diff --git a/BookingMicroservice/Services/IReservationComplianceService.cs b/BookingMicroservice/Services/IReservationComplianceService.cs index 98364a189804ab74a6b2acdd01f23307fae690e8..7f82720510a1f07e8016add0940502dd22bf822c 100644 --- a/BookingMicroservice/Services/IReservationComplianceService.cs +++ b/BookingMicroservice/Services/IReservationComplianceService.cs @@ -4,7 +4,7 @@ namespace BookingMicroservice.Services { public interface IReservationComplianceService { - Task<Booking> TryCreateBookingAsync(int flightId, int userId, BookingClass bookingClass, int? seatId); + Task<Booking?> TryCreateBookingAsync(int flightId, int userId, BookingClass bookingClass, int? seatId); Task TryBookSeatAsync(int bookingId, int seatId); } diff --git a/BookingMicroservice/Services/ReservationComplianceService.cs b/BookingMicroservice/Services/ReservationComplianceService.cs index 6aa02963716fa70d867b486c07d80b3f4b82e408..df3f6a6effa87f3aa15568b52b339ec3456a81fb 100644 --- a/BookingMicroservice/Services/ReservationComplianceService.cs +++ b/BookingMicroservice/Services/ReservationComplianceService.cs @@ -10,7 +10,7 @@ namespace BookingMicroservice.Services private readonly IFlightServiceClient flightServiceClient; private const string NO_AVAILABLE_SEAT_MSG = "Unfortunately, there are no {0} seats available for this flight. Please try selecting a different class, flight, or date."; - private const string BOOKED_SEAT_MSG = "The selected seat is already booked. Please choose a different seat"; + private const string BOOKED_SEAT_MSG = "The selected seat is already booked. Please choose a different seat."; public ReservationComplianceService(IBookingService bookingService, IFlightServiceClient flightServiceClient) { @@ -18,7 +18,7 @@ namespace BookingMicroservice.Services this.flightServiceClient = flightServiceClient; } - public async Task<Booking> TryCreateBookingAsync(int flightId, int userId, BookingClass bookingClass, int? seatId) + public async Task<Booking?> TryCreateBookingAsync(int flightId, int userId, BookingClass bookingClass, int? seatId) { HttpResponseMessage capacityResponse = await flightServiceClient.GetFlightCapacityAsync(flightId, (int)bookingClass); if (!capacityResponse.IsSuccessStatusCode) @@ -29,13 +29,28 @@ namespace BookingMicroservice.Services throw new InvalidOperationException("Invalid capacity value received from flight service."); // Get current bookings for the flight and class - //int currentBookings = bookingService.GetBookingForFlight(flightId).Count(booking => booking.BookingClass == bookingClass); int currentBookings = bookingService.GetBookings(flightId: flightId, bookingClass: bookingClass).Count(); if (currentBookings >= capacity) throw new BookingException(string.Format(NO_AVAILABLE_SEAT_MSG, bookingClass.ToString().ToLower())); - return bookingService.CreateBooking(flightId, userId, bookingClass, seatId); + Booking booking = bookingService.CreateBooking(flightId, userId, bookingClass); + if(seatId.HasValue && booking?.Id != null) + { + try + { + await TryBookSeatAsync(booking.Id, seatId.Value); + booking.SetSeatNumber(seatId.Value); + } + catch (Exception exception) + { + // delete booking as there has been a failure in booking the seat + bookingService.DeleteBooking(booking.Id); + throw new InvalidOperationException($@"Booking has been cancelled as there has been an issue with selecting the seat.{Environment.NewLine}{exception.Message}{Environment.NewLine}Please try again."); + } + } + + return booking; } public async Task TryBookSeatAsync(int bookingId, int seatId) @@ -52,10 +67,11 @@ namespace BookingMicroservice.Services throw new BookingException(BOOKED_SEAT_MSG); HttpResponseMessage seatBookingRespone = await flightServiceClient.BookSeatAsync(seatId); - string capacityString = await seatBookingRespone.Content.ReadAsStringAsync(); // remove this later - if (!seatAvailabilityResponse.IsSuccessStatusCode) - throw new InvalidOperationException("Error booking seat."); + if (!seatBookingRespone.IsSuccessStatusCode) { + string? failureMessage = seatBookingRespone.ReasonPhrase; + throw new InvalidOperationException($"Error booking seat; {failureMessage}"); + } bookingService.UpdateBooking(bookingId, seatId); } diff --git a/FlightMicroservice/Controllers/FlightController.cs b/FlightMicroservice/Controllers/FlightController.cs index ea3411bda1e1510ea3269969c853d6844a58e385..649e10418beb7922f494983520f430ab359ab7b4 100644 --- a/FlightMicroservice/Controllers/FlightController.cs +++ b/FlightMicroservice/Controllers/FlightController.cs @@ -64,7 +64,7 @@ namespace FlightMicroservice.Controllers int seatCapacity = classType == ClassType.BUSINESS ? flight.BusinessCapacity : flight.EconomyCapacity; - return Ok(new { ClassType = classType.ToString(), Capacity = seatCapacity }); + return Ok(seatCapacity); } diff --git a/FlightMicroservice/Controllers/SeatController.cs b/FlightMicroservice/Controllers/SeatController.cs index 47491f7bad0747f8eee64445dea864beb54a24b3..758bffff97652ceae598f74790737a3432c1868a 100644 --- a/FlightMicroservice/Controllers/SeatController.cs +++ b/FlightMicroservice/Controllers/SeatController.cs @@ -58,7 +58,7 @@ namespace FlightMicroservice.Controllers try { bool isAvailable = seatService.IsAvailable(id); - return Ok(new { IsAvailable = isAvailable }); + return Ok(isAvailable); } catch (KeyNotFoundException ex) { diff --git a/docker-compose.yml b/docker-compose.yml index 109ae714e84c633d4d1f750a619d017a61d67d84..ac1f7f9e747fcc7acf4ed65b276c6a5e22279072 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,16 +47,16 @@ services: ports: - "${CLIENT_PORT}:4200" - db: - image: mysql:${MYSQL_IMAGE_TAG} - command: --default-authentication-plugin=mysql_native_password - restart: always - environment: - MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} - MYSQL_DATABASE: ${DB_NAME} - MYSQL_USER: ${DB_USER} - MYSQL_PASSWORD: ${DB_PASSWORD} - volumes: - - ./Database:/docker-entrypoint-initdb.d - ports: - - "${DB_PORT}:3306" + # db: + # image: mysql:${MYSQL_IMAGE_TAG} + # command: --default-authentication-plugin=mysql_native_password + # restart: always + # environment: + # MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + # MYSQL_DATABASE: ${DB_NAME} + # MYSQL_USER: ${DB_USER} + # MYSQL_PASSWORD: ${DB_PASSWORD} + # volumes: + # - ./Database:/docker-entrypoint-initdb.d + # ports: + # - "${DB_PORT}:3306"