diff --git a/.env b/.env index 09923e3d32fed99977efd11b929861244780c93b..673e590786e96e46a0ea58e0f4a06002a86fd218 100644 --- a/.env +++ b/.env @@ -1,12 +1,17 @@ # Image settings IMAGE_TAG=0.0.1 +MYSQL_IMAGE_TAG=8.0 + # Database configuration -DB_PORT=3308 -DB_NAME=AspNetCoreDb -DB_USER=root -DB_PASSWORD= +DB_PORT=3307 +DB_NAME=myDB +DB_USER=user +DB_PASSWORD=user DB_CHARSET=utf8mb4 # Service ports -USER_MICROSERVICE_PORT=5089 # This port will be used as the JWT issuer +USER_MICROSERVICE_PORT=5089 +FLIGHT_MICROSERVICE_PORT=5175 +GATEWAY_API_PORT=5267 +CLIENT_PORT=4200 \ No newline at end of file diff --git a/Database/InitTables.sql b/Database/InitTables.sql new file mode 100644 index 0000000000000000000000000000000000000000..6805d233e8cd62079b3e81de3c91104ab1e61098 --- /dev/null +++ b/Database/InitTables.sql @@ -0,0 +1,34 @@ +CREATE TABLE Users ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Username LONGTEXT NOT NULL, + Email LONGTEXT NOT NULL, + PasswordHash LONGTEXT NOT NULL, + Type INT NOT NULL +); + +CREATE TABLE RefreshTokens ( + Id INT AUTO_INCREMENT PRIMARY KEY, + UserId INT NOT NULL, + Token LONGTEXT NOT NULL, + ExpirationDate DATETIME(6) NOT NULL +); + +CREATE TABLE Flights ( + Id INT AUTO_INCREMENT PRIMARY KEY, + Origin LONGTEXT NOT NULL, + Destination LONGTEXT NOT NULL, + DepartureTime DATETIME(6) NOT NULL, + ArrivalTime DATETIME(6) NOT NULL, + EconomyCapacity INT NOT NULL, + BusinessCapacity INT NOT NULL, + EconomyPrice DECIMAL(65, 30) NOT NULL, + BusinessPrice DECIMAL(65, 30) NOT NULL +); + +CREATE TABLE Seats ( + Id INT AUTO_INCREMENT PRIMARY KEY, + FlightId INT NOT NULL, + SeatNumber LONGTEXT NOT NULL, + ClassType INT NOT NULL, + IsAvailable TINYINT(1) NOT NULL +); diff --git a/GatewayAPI/.dockerignore b/GatewayAPI/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..fe1152bdb8442f4d14f9b9533e63fe0c2680bcee --- /dev/null +++ b/GatewayAPI/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/GatewayAPI/Clients/FlightService/FlightServiceClient.cs b/GatewayAPI/Clients/FlightService/FlightServiceClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..cdf5f3a75248f64842cd7cc1bde1d9a0879f1a66 --- /dev/null +++ b/GatewayAPI/Clients/FlightService/FlightServiceClient.cs @@ -0,0 +1,75 @@ +using GatewayAPI.Models; + +namespace GatewayAPI.Clients.FlightService +{ + public class FlightServiceClient : IFlightServiceClient + { + private readonly HttpClient httpClient; + private static readonly string FLIGHT_API_PATH = "api/Flight"; + private static readonly string SEAT_API_PATH = "api/Seat"; + + public FlightServiceClient(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + public async Task<HttpResponseMessage> GetFlightAsync(int flightId) + { + return await httpClient.GetAsync($"{FLIGHT_API_PATH}/{flightId}"); + } + + public async Task<HttpResponseMessage> GetFlightsAsync(string? origin = null, string? destination = null, DateTime? departureTime = null, DateTime? arrivalTime = null) + { + var queryParams = new List<string>(); + + if (origin != null) + queryParams.Add($"origin={Uri.EscapeDataString(origin)}"); + + if (destination != null) + queryParams.Add($"destination={Uri.EscapeDataString(destination)}"); + + if (departureTime.HasValue) + queryParams.Add($"departureTime={departureTime.Value.ToString("yyyy-MM-dd HH:mm:ss.fff") + "000"}"); + + if (arrivalTime.HasValue) + queryParams.Add($"arrivalTime={arrivalTime.Value.ToString("yyyy-MM-dd HH:mm:ss.fff") + "000"}"); + + string queryString = queryParams.Any() ? $"?{string.Join("&", queryParams)}" : string.Empty; + + return await httpClient.GetAsync($"{FLIGHT_API_PATH}{queryString}"); + } + + public async Task<HttpResponseMessage> AddFlightAsync(FlightCreation flight) + { + return await httpClient.PostAsJsonAsync(FLIGHT_API_PATH, flight); + } + + public async Task<HttpResponseMessage> GetFlightCapacityAsync(int flightId, int classType) + { + return await httpClient.GetAsync($"{FLIGHT_API_PATH}/{flightId}/capacity?ClassType={classType}"); + } + + + + + public async Task<HttpResponseMessage> GetSeatsAsync() + { + return await httpClient.GetAsync(SEAT_API_PATH); + } + + public async Task<HttpResponseMessage> GetSeatAsync(int seatId) + { + return await httpClient.GetAsync($"{SEAT_API_PATH}/{seatId}"); + } + + public async Task<HttpResponseMessage> IsSeatAvailableAsync(int seatId) + { + return await httpClient.GetAsync($"{SEAT_API_PATH}/{seatId}/isAvailable"); + } + + public async Task<HttpResponseMessage> BookSeatAsync(int seatId) + { + return await httpClient.PutAsync($"{SEAT_API_PATH}/{seatId}", null); + } + } +} diff --git a/GatewayAPI/Clients/FlightService/IFlightServiceClient.cs b/GatewayAPI/Clients/FlightService/IFlightServiceClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..def17a9d8dc494c5e1dd6242f36ff299e3b954a2 --- /dev/null +++ b/GatewayAPI/Clients/FlightService/IFlightServiceClient.cs @@ -0,0 +1,18 @@ +using GatewayAPI.Models; + +namespace GatewayAPI.Clients.FlightService +{ + public interface IFlightServiceClient + { + Task<HttpResponseMessage> GetFlightAsync(int flightId); + Task<HttpResponseMessage> GetFlightsAsync(string? origin = null, string? destination = null, DateTime? departureTime = null, DateTime? arrivalTime = null); + Task<HttpResponseMessage> AddFlightAsync(FlightCreation flight); + Task<HttpResponseMessage> GetFlightCapacityAsync(int flightId, int classType); + + + Task<HttpResponseMessage> GetSeatsAsync(); + Task<HttpResponseMessage> GetSeatAsync(int seatId); + Task<HttpResponseMessage> IsSeatAvailableAsync(int seatId); + Task<HttpResponseMessage> BookSeatAsync(int seatId); + } +} diff --git a/GatewayAPI/Clients/UserService/IUserServiceClient.cs b/GatewayAPI/Clients/UserService/IUserServiceClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..dfcaeb7ba7c830d08f4d0baa639349d0a104685c --- /dev/null +++ b/GatewayAPI/Clients/UserService/IUserServiceClient.cs @@ -0,0 +1,15 @@ +using GatewayAPI.Models; + +namespace GatewayAPI.Clients.UserService +{ + public interface IUserServiceClient + { + Task<HttpResponseMessage> GetUserAsync(int id); + Task<HttpResponseMessage> GetUsersAsync(); + Task<HttpResponseMessage> RegisterUserAsync(UserRegistration user); + Task<HttpResponseMessage> LoginUserAsync(UserLogin user); + Task<HttpResponseMessage> AuthorizeUserAsync(); + Task<HttpResponseMessage> LogoutUserAsync(); + Task<HttpResponseMessage> UpdateUserAsync(int id, UserUpdateInfo updateInfo); + } +} diff --git a/GatewayAPI/Clients/UserService/UserServiceClient.cs b/GatewayAPI/Clients/UserService/UserServiceClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..46752a45606c68ef74aee88de8a42598d4d0dea4 --- /dev/null +++ b/GatewayAPI/Clients/UserService/UserServiceClient.cs @@ -0,0 +1,54 @@ +using GatewayAPI.Models; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; + +namespace GatewayAPI.Clients.UserService +{ + public class UserServiceClient : IUserServiceClient + { + private readonly HttpClient httpClient; + private static readonly string API_PATH = "api/User"; + + public UserServiceClient(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + public async Task<HttpResponseMessage> GetUserAsync(int id) + { + return await httpClient.GetAsync($"{API_PATH}/{id}"); + } + + public async Task<HttpResponseMessage> GetUsersAsync() + { + return await httpClient.GetAsync($"{API_PATH}"); + } + + public async Task<HttpResponseMessage> RegisterUserAsync(UserRegistration user) + { + return await httpClient.PostAsJsonAsync($"{API_PATH}/register", user); + } + + public async Task<HttpResponseMessage> LoginUserAsync(UserLogin user) + { + return await httpClient.PostAsJsonAsync($"{API_PATH}/login", user); + } + + public async Task<HttpResponseMessage> AuthorizeUserAsync() + { + return await httpClient.PostAsync($"{API_PATH}/authorize", null); + } + + public async Task<HttpResponseMessage> LogoutUserAsync() + { + return await httpClient.PostAsync($"{API_PATH}/logout", null); + } + + public async Task<HttpResponseMessage> UpdateUserAsync(int id, UserUpdateInfo updateInfo) + { + return await httpClient.PatchAsJsonAsync($"{API_PATH}/{id}", updateInfo); + } + + } +} diff --git a/GatewayAPI/Controllers/FlightController.cs b/GatewayAPI/Controllers/FlightController.cs new file mode 100644 index 0000000000000000000000000000000000000000..e335bd102327e36d733c4b3e59b23020fa25182a --- /dev/null +++ b/GatewayAPI/Controllers/FlightController.cs @@ -0,0 +1,47 @@ +using GatewayAPI.Clients.FlightService; +using GatewayAPI.Clients.UserService; +using GatewayAPI.Models; +using Microsoft.AspNetCore.Mvc; + +namespace GatewayAPI.Controllers +{ + [ApiController] + [Route("api/[Controller]")] + public class FlightController : ControllerBase + { + private readonly IFlightServiceClient flightServiceClient; + public FlightController(IFlightServiceClient flightServiceClient) + { + this.flightServiceClient = flightServiceClient; + } + + [HttpGet()] + public async Task<IActionResult> GetFlights(string? origin = null, string? destination = null, DateTime? departureTime = null, DateTime? arrivalTime = null) + { + HttpResponseMessage response = await flightServiceClient.GetFlightsAsync(origin, destination, departureTime, arrivalTime); + return new HttpResponseMessageResult(response); + } + + [HttpGet("{id}")] + public async Task<IActionResult> GetFlights(int id) + { + HttpResponseMessage response = await flightServiceClient.GetFlightAsync(id); + return new HttpResponseMessageResult(response); + } + + [HttpPost()] + public async Task<IActionResult> AddFlight([FromBody] FlightCreation flightCreation) + { + HttpResponseMessage response = await flightServiceClient.AddFlightAsync(flightCreation); + return new HttpResponseMessageResult(response); + } + + + [HttpGet("{id}/capacity")] + public async Task<IActionResult> GetFlightCapacity([FromRoute] int id, [FromQuery] int classType) + { + HttpResponseMessage response = await flightServiceClient.GetFlightCapacityAsync(id, classType); + return new HttpResponseMessageResult(response); + } + } +} diff --git a/GatewayAPI/Controllers/SeatController.cs b/GatewayAPI/Controllers/SeatController.cs new file mode 100644 index 0000000000000000000000000000000000000000..750dfa67d086a9b8eded50d22a24ab1a469131be --- /dev/null +++ b/GatewayAPI/Controllers/SeatController.cs @@ -0,0 +1,45 @@ +using GatewayAPI.Clients.FlightService; +using Microsoft.AspNetCore.Mvc; + +namespace GatewayAPI.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class SeatController : ControllerBase + { + private readonly IFlightServiceClient flightServiceClient; + public SeatController(IFlightServiceClient flightServiceClient) + { + this.flightServiceClient = flightServiceClient; + } + + [HttpGet()] + public async Task<IActionResult> GetSeats() + { + HttpResponseMessage response = await flightServiceClient.GetSeatsAsync(); + return new HttpResponseMessageResult(response); + } + + [HttpGet("{id}")] + public async Task<IActionResult> GetSeats(int id) + { + HttpResponseMessage response = await flightServiceClient.GetSeatAsync(id); + return new HttpResponseMessageResult(response); + } + + [HttpPut("{id}")] + public async Task<IActionResult> BookSeat(int id) + { + HttpResponseMessage response = await flightServiceClient.BookSeatAsync(id); + return new HttpResponseMessageResult(response); + } + + [HttpGet("{id}/isAvailable")] + public async Task<IActionResult> IsAvailable(int id) + { + HttpResponseMessage response = await flightServiceClient.IsSeatAvailableAsync(id); + return new HttpResponseMessageResult(response); + } + + } +} diff --git a/GatewayAPI/Controllers/UserController.cs b/GatewayAPI/Controllers/UserController.cs new file mode 100644 index 0000000000000000000000000000000000000000..2061f69c6f09c131ffce64f62e6a06a79b76513e --- /dev/null +++ b/GatewayAPI/Controllers/UserController.cs @@ -0,0 +1,68 @@ +using GatewayAPI.Clients.UserService; +using GatewayAPI.Models; +using Microsoft.AspNetCore.Mvc; +using System.Text; + +namespace GatewayAPI.Controllers +{ + [ApiController] + [Route("api/[Controller]")] + public class UserController : ControllerBase + { + private readonly IUserServiceClient userServiceClient; + public UserController(IUserServiceClient userServiceClient) + { + this.userServiceClient = userServiceClient; + } + + [HttpPost("register")] + public async Task<IActionResult> Register([FromBody] UserRegistration userRegistration) + { + HttpResponseMessage response = await userServiceClient.RegisterUserAsync(userRegistration); + return new HttpResponseMessageResult(response); + } + + [HttpPost("authorize")] + public async Task<IActionResult> Authorize() + { + HttpResponseMessage response = await userServiceClient.AuthorizeUserAsync(); + return new HttpResponseMessageResult(response); + } + + [HttpPost("login")] + public async Task<IActionResult> Login([FromBody] UserLogin userLogin) + { + HttpResponseMessage response = await userServiceClient.LoginUserAsync(userLogin); + return new HttpResponseMessageResult(response); + } + + [HttpPost("logout")] + public async Task<IActionResult> Logout() + { + HttpResponseMessage response = await userServiceClient.LogoutUserAsync(); + return new HttpResponseMessageResult(response); + } + + [HttpGet()] + public async Task<IActionResult> GetUsers() + { + HttpResponseMessage response = await userServiceClient.GetUsersAsync(); + return new HttpResponseMessageResult(response); + } + + [HttpGet("{id}")] + public async Task<IActionResult> GetUser(int id) + { + HttpResponseMessage response = await userServiceClient.GetUserAsync(id); + return new HttpResponseMessageResult(response); + } + + [HttpPatch("{id}")] + public async Task<IActionResult> UpdateUser(int id, [FromBody] UserUpdateInfo updateInfo) + { + HttpResponseMessage response = await userServiceClient.UpdateUserAsync(id, updateInfo); + return new HttpResponseMessageResult(response); + } + } + +} diff --git a/GatewayAPI/Dockerfile b/GatewayAPI/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..91552e7d806230d9e91a2ac38b4a877899d87b03 --- /dev/null +++ b/GatewayAPI/Dockerfile @@ -0,0 +1,25 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["GatewayAPI.csproj", "."] +RUN dotnet restore "./GatewayAPI.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "./GatewayAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./GatewayAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "GatewayAPI.dll"] \ No newline at end of file diff --git a/GatewayAPI/GatewayAPI.csproj b/GatewayAPI/GatewayAPI.csproj new file mode 100644 index 0000000000000000000000000000000000000000..a6c8e7aed19e4a88d321cabd92a6bc86dbc715f6 --- /dev/null +++ b/GatewayAPI/GatewayAPI.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <UserSecretsId>50dbee94-e07d-4906-85d3-8337755e0027</UserSecretsId> + <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> + <DockerfileContext>.</DockerfileContext> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> + </ItemGroup> + +</Project> diff --git a/GatewayAPI/GatewayAPI.csproj.user b/GatewayAPI/GatewayAPI.csproj.user new file mode 100644 index 0000000000000000000000000000000000000000..983ecfc07a3622c3f9e0e73b96ae3e61aada8cb9 --- /dev/null +++ b/GatewayAPI/GatewayAPI.csproj.user @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ActiveDebugProfile>http</ActiveDebugProfile> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <DebuggerFlavor>ProjectDebugger</DebuggerFlavor> + </PropertyGroup> +</Project> \ No newline at end of file diff --git a/GatewayAPI/GatewayAPI.http b/GatewayAPI/GatewayAPI.http new file mode 100644 index 0000000000000000000000000000000000000000..a5bc22a7d7b0f11192f97786eee250cd270ef58a --- /dev/null +++ b/GatewayAPI/GatewayAPI.http @@ -0,0 +1,6 @@ +@GatewayAPI_HostAddress = http://localhost:5267 + +GET {{GatewayAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/GatewayAPI/GatewayAPI.sln b/GatewayAPI/GatewayAPI.sln new file mode 100644 index 0000000000000000000000000000000000000000..018f984ffaca373a57f72a68095a682c7e8f0b3e --- /dev/null +++ b/GatewayAPI/GatewayAPI.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatewayAPI", "GatewayAPI.csproj", "{2ABFA760-35A9-4BF8-ADAF-3751CCC8C20A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2ABFA760-35A9-4BF8-ADAF-3751CCC8C20A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2ABFA760-35A9-4BF8-ADAF-3751CCC8C20A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2ABFA760-35A9-4BF8-ADAF-3751CCC8C20A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2ABFA760-35A9-4BF8-ADAF-3751CCC8C20A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7FFD5B8C-F828-4EBE-B2B7-888D63CDE589} + EndGlobalSection +EndGlobal diff --git a/GatewayAPI/HttpResponseMessageResult.cs b/GatewayAPI/HttpResponseMessageResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..3873c553f6453a6f17bf9731c31873f9c5b02417 --- /dev/null +++ b/GatewayAPI/HttpResponseMessageResult.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; +using System.Net.Http; +using System.Threading.Tasks; + +namespace GatewayAPI +{ + public class HttpResponseMessageResult : IActionResult + { + private readonly HttpResponseMessage responseMessage; + + public HttpResponseMessageResult(HttpResponseMessage responseMessage) + { + this.responseMessage = responseMessage ?? throw new ArgumentNullException(nameof(responseMessage)); + } + + public async Task ExecuteResultAsync(ActionContext context) + { + HttpContext httpContext = context.HttpContext; + HttpResponse response = httpContext.Response; + + response.ContentType = "application/json; charset=utf-8"; + + // Copy the status code + response.StatusCode = (int)responseMessage.StatusCode; + + // Copy the cookies + if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookieValues)) + { + foreach (string cookie in cookieValues) + response.Headers.Append("Set-Cookie", cookie); + } + + // Copy the response body directly to the response stream + using (Stream responseStream = await responseMessage.Content.ReadAsStreamAsync()) + { + await responseStream.CopyToAsync(response.Body); + await response.Body.FlushAsync(); + } + } + } +} diff --git a/GatewayAPI/Models/FlightCreation.cs b/GatewayAPI/Models/FlightCreation.cs new file mode 100644 index 0000000000000000000000000000000000000000..e3bdfa48d381f6c4c91fee0691b517b1a0ab763d --- /dev/null +++ b/GatewayAPI/Models/FlightCreation.cs @@ -0,0 +1,15 @@ +namespace GatewayAPI.Models +{ + public class FlightCreation + { + public required string Origin { get; set; } + public required string Destination { get; set; } + public required DateTime DepartureTime { get; set; } + public required DateTime ArrivalTime { get; set; } + public required int EconomyCapacity { get; set; } + public required int BusinessCapacity { get; set; } + public required decimal EconomyPrice { get; set; } + public required decimal BusinessPrice { get; set; } + + } +} diff --git a/GatewayAPI/Models/UserLogin.cs b/GatewayAPI/Models/UserLogin.cs new file mode 100644 index 0000000000000000000000000000000000000000..47ae2b3b7eea2c4f248314bec714506cc59dcc01 --- /dev/null +++ b/GatewayAPI/Models/UserLogin.cs @@ -0,0 +1,9 @@ +namespace GatewayAPI.Models +{ + public class UserLogin + { + public required string Username { get; set; } + public required string Password { get; set; } + + } +} diff --git a/GatewayAPI/Models/UserRegistration.cs b/GatewayAPI/Models/UserRegistration.cs new file mode 100644 index 0000000000000000000000000000000000000000..dc94c025e13b9b705c39b251ee69d33e8fbc2d3f --- /dev/null +++ b/GatewayAPI/Models/UserRegistration.cs @@ -0,0 +1,11 @@ +namespace GatewayAPI.Models +{ + public class UserRegistration + { + public required string Username { get; set; } + public required string Email { get; set; } + public required string Password { get; set; } + public required int UserType { get; set; } + + } +} diff --git a/GatewayAPI/Models/UserUpdateInfo.cs b/GatewayAPI/Models/UserUpdateInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf4c9c3b88fce116182686f62ceb7e3051c3fcd5 --- /dev/null +++ b/GatewayAPI/Models/UserUpdateInfo.cs @@ -0,0 +1,9 @@ +namespace GatewayAPI.Models +{ + public class UserUpdateInfo + { + public string? Username { get; set; } + public string? Email { get; set; } + public string? Password { get; set; } + } +} diff --git a/GatewayAPI/Program.cs b/GatewayAPI/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..8f7722be178b40d5bf7ba13ae48ee1f711801caf --- /dev/null +++ b/GatewayAPI/Program.cs @@ -0,0 +1,27 @@ +using GatewayAPI; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddHttpContextAccessor(); +builder.Services.AddTransient<RequestCookieHandler>(); +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Add Http Typed Clients for each Microservice +builder.Services.AddHttpClients(builder.Configuration); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +//app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); diff --git a/GatewayAPI/Properties/launchSettings.json b/GatewayAPI/Properties/launchSettings.json new file mode 100644 index 0000000000000000000000000000000000000000..bf79d5709995c33286eba3471cf4872823ff1759 --- /dev/null +++ b/GatewayAPI/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5267" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7130;http://localhost:5267" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52758", + "sslPort": 44344 + } + } +} \ No newline at end of file diff --git a/GatewayAPI/RequestCookieHandler .cs b/GatewayAPI/RequestCookieHandler .cs new file mode 100644 index 0000000000000000000000000000000000000000..11a59596adc6a9ae9a0a51f2e6904f42f09fe4bc --- /dev/null +++ b/GatewayAPI/RequestCookieHandler .cs @@ -0,0 +1,36 @@ +using System.Net; + +namespace GatewayAPI +{ + 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/GatewayAPI/ServiceCollectionExtensions.cs b/GatewayAPI/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..7235c816dcbe5767cfb5c3ef2ccdce5d583e42c4 --- /dev/null +++ b/GatewayAPI/ServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +using GatewayAPI.Clients.FlightService; +using GatewayAPI.Clients.UserService; +using System.Runtime.CompilerServices; + +namespace GatewayAPI +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddHttpClients(this IServiceCollection services, ConfigurationManager configurationManager) + { + services.AddHttpClient<IUserServiceClient, UserServiceClient>(client => + { + string baseUrl = configurationManager["UserMicroservice:BaseUrl"] ?? throw new InvalidOperationException("UserMicroservice BaseUrl is not configured."); + client.BaseAddress = new Uri(baseUrl.EndsWith("/") ? baseUrl : baseUrl + "/"); + }).AddHttpMessageHandler<RequestCookieHandler>(); + + services.AddHttpClient<IFlightServiceClient, FlightServiceClient>(client => + { + string baseUrl = configurationManager["FlightMicroservice:BaseUrl"] ?? throw new InvalidOperationException("FlightMicroservice BaseUrl is not configured."); + client.BaseAddress = new Uri(baseUrl.EndsWith("/") ? baseUrl : baseUrl + "/"); + }).AddHttpMessageHandler<RequestCookieHandler>(); + + return services; + } + } +} diff --git a/GatewayAPI/appsettings.Development.json b/GatewayAPI/appsettings.Development.json new file mode 100644 index 0000000000000000000000000000000000000000..0c208ae9181e5e5717e47ec1bd59368aebc6066e --- /dev/null +++ b/GatewayAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/GatewayAPI/appsettings.json b/GatewayAPI/appsettings.json new file mode 100644 index 0000000000000000000000000000000000000000..f0c8836097302b9aad3a4302088f49f88bc4e46a --- /dev/null +++ b/GatewayAPI/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "UserMicroservice": { + "BaseUrl": "http://localhost:5089" + }, + "FlightMicroservice": { + "BaseUrl": "http://localhost:5175" + } +} diff --git a/UserMicroservice/Controllers/UserController.cs b/UserMicroservice/Controllers/UserController.cs index ea86b63444c803bc1bd97876e7df6cc79e646e33..b14e10802f0ae5c083d00f51021e68e12bfc09dd 100644 --- a/UserMicroservice/Controllers/UserController.cs +++ b/UserMicroservice/Controllers/UserController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using System.Security.Claims; using UserMicroservice.Models; using UserMicroservice.Services; @@ -31,9 +32,10 @@ namespace UserMicroservice.Controllers if (user == null) return BadRequest(); - return authenticateUser(user.Id); - - } catch (InvalidOperationException ex) + setAuthCookies(user.Id); + return Ok(new { user.Id, user.Username, user.Email, user.Type }); + } + catch (InvalidOperationException ex) { return BadRequest(ex.Message); } @@ -57,7 +59,8 @@ namespace UserMicroservice.Controllers if (!int.TryParse(userIdString, out int userId)) return BadRequest("User ID is invalid."); - return authenticateUser(userId); + setAuthCookies(userId); + return Ok(); } // POST: api/User/login @@ -68,14 +71,15 @@ namespace UserMicroservice.Controllers if(user == null) return Unauthorized(); - return authenticateUser(user.Id); + setAuthCookies(user.Id); + return Ok(new { user.Id, user.Username, user.Email, user.Type }); } - private IActionResult authenticateUser(int userId) + private void setAuthCookies(int userId) { AuthTokenPair authToken = _authService.AuthenticateUser(userId); if (authToken == null) - return BadRequest(); + throw new ArgumentNullException(nameof(authToken)); // Set the access token as an HttpOnly cookie Response.Cookies.Append("AccessToken", authToken.AccessToken, new CookieOptions @@ -94,8 +98,6 @@ namespace UserMicroservice.Controllers SameSite = SameSiteMode.Strict, Expires = DateTimeOffset.UtcNow.AddDays(2) }); - - return Ok(); } // POST: api/User/logout @@ -153,8 +155,28 @@ namespace UserMicroservice.Controllers User? user = _userService.GetUser(id); if(user == null) return NotFound($"User with {id} doesnt exist"); - - return Ok(user); + + return Ok(new{ user.Id, user.Username, user.Email, user.Type }); + } + + // PUT: api/User/{id} + [Authorize] + [HttpPatch("{id}")] + public IActionResult UpdateUser(int id, [FromBody] UpdateUserModel model) + { + try + { + _userService.UpdateUser(id, model.Username, model.Email, model.Password); + return Ok(); + } + catch (KeyNotFoundException exception) + { + return NotFound(exception.Message); + } + catch (DbUpdateException exception) + { + return BadRequest(exception.Message); + } } } diff --git a/UserMicroservice/Models/UpdateUserModel.cs b/UserMicroservice/Models/UpdateUserModel.cs new file mode 100644 index 0000000000000000000000000000000000000000..bda556ab8afd0285533f5a565d9ff8caf646e635 --- /dev/null +++ b/UserMicroservice/Models/UpdateUserModel.cs @@ -0,0 +1,9 @@ +namespace UserMicroservice.Models +{ + public class UpdateUserModel + { + public string? Username { get; set; } + public string? Email { get; set; } + public string? Password { get; set; } + } +} diff --git a/UserMicroservice/Services/IUserService.cs b/UserMicroservice/Services/IUserService.cs index 2a4e75bc8dffa98a58c9d1f1925491aebecd1730..0e6deffc39d5b7b37e3778f0c96882df235b1878 100644 --- a/UserMicroservice/Services/IUserService.cs +++ b/UserMicroservice/Services/IUserService.cs @@ -10,7 +10,7 @@ namespace UserMicroservice.Services User? GetUser(string username, string password); List<User> GetUsers(); User CreateUser(string email, string userName, string password, UserType UserType); - User UpdateUser(User updatedUser); + void UpdateUser(int id, string? username, string? email, string? password); bool DeleteUser(string username); User? GetUserByEmail(string email); } diff --git a/UserMicroservice/Services/UserService.cs b/UserMicroservice/Services/UserService.cs index 9288ae8b3754d62c7759fe1e02eff482e8574b11..59c0f37697e912984c773e7ea882771b3a4e1030 100644 --- a/UserMicroservice/Services/UserService.cs +++ b/UserMicroservice/Services/UserService.cs @@ -80,17 +80,32 @@ namespace UserMicroservice.Services return user; } - public User UpdateUser(User updatedUser) + public void UpdateUser(int id, string? username, string? email, string? password) { - _context.Users - .Where(user => user.Id == updatedUser.Id) + User? user = GetUser(id); + if(user == null) + throw new KeyNotFoundException($"A User with the provided Id {id} doesnt exist"); + + string updatedEmail = string.IsNullOrEmpty(email) ? user.Email : email; + string updatedUserName = string.IsNullOrEmpty(username) ? user.Username : username; + string updatedPassword = user.PasswordHash; + + if (!string.IsNullOrEmpty(password)) { + user.SetPasswordHash(_passwordHasher, password); + updatedPassword = user.PasswordHash; + } + + int affectedRows = _context.Users + .Where(user => user.Id == id) .ExecuteUpdate(setters => setters - .SetProperty(user => user.Username, updatedUser.Username) - .SetProperty(user => user.PasswordHash, updatedUser.PasswordHash)); + .SetProperty(user => user.Username, updatedUserName) + .SetProperty(user => user.Email, updatedEmail) + .SetProperty(user => user.PasswordHash, updatedPassword)); _context.SaveChanges(); - return updatedUser; + if (affectedRows == 0) + throw new DbUpdateException("Operation was not able to update the Database"); } } } diff --git a/docker-compose.yml b/docker-compose.yml index 83dca104e1be91f60ddf7a9f28865f2269c9ab72..109ae714e84c633d4d1f750a619d017a61d67d84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ version: '3.8' - services: usermicroservice: build: @@ -12,30 +11,52 @@ services: - DOTNET_RUNNING_IN_CONTAINER=true - ConnectionStrings__DefaultConnection=Server=host.docker.internal;Port=${DB_PORT};Database=${DB_NAME};User=${DB_USER};Password=${DB_PASSWORD};CharSet=${DB_CHARSET} - Jwt__Key=0QTrd3jToEYj205k01A2R87Hc5YpqDNeywg7JzQpczs= - - Jwt__Issuer=http://localhost:${USER_MICROSERVICE_PORT} - - Jwt__Audience=http://localhost:${USER_MICROSERVICE_PORT} + - Jwt__Issuer=http://usermicroservice:8080 + - Jwt__Audience=http://usermicroservice:8080 flightmicroservice: build: context: ./FlightMicroservice image: flightmicroservice:${IMAGE_TAG} ports: - - "5175:8080" + - "${FLIGHT_MICROSERVICE_PORT}:8080" environment: - ASPNETCORE_ENVIRONMENT=Production - DOTNET_RUNNING_IN_CONTAINER=true - ConnectionStrings__DefaultConnection=Server=host.docker.internal;Port=${DB_PORT};Database=${DB_NAME};User=${DB_USER};Password=${DB_PASSWORD};CharSet=${DB_CHARSET} - Jwt__Key=0QTrd3jToEYj205k01A2R87Hc5YpqDNeywg7JzQpczs= - - Jwt__Issuer=http://localhost:${USER_MICROSERVICE_PORT} - - Jwt__Audience=http://localhost:${USER_MICROSERVICE_PORT} + - Jwt__Issuer=http://usermicroservice:8080 + - Jwt__Audience=http://usermicroservice:8080 + + gatewayapi: + build: + context: ./GatewayAPI + image: gatewayapi:${IMAGE_TAG} + ports: + - "${GATEWAY_API_PORT}:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - DOTNET_RUNNING_IN_CONTAINER=true + - UserMicroservice__BaseUrl=http://usermicroservice:8080 + - FlightMicroservice__BaseUrl=http://flightmicroservice:8080 client: build: context: ./client image: client:${IMAGE_TAG} ports: - - "4200:4200" - - + - "${CLIENT_PORT}:4200" -# ... other services + 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"