From 7d0863c3392918a52a81ae262868c150911a733d Mon Sep 17 00:00:00 2001 From: Mouaz Abdelsamad <ma03081@surrey.ac.uk> Date: Tue, 26 Mar 2024 17:24:19 +0000 Subject: [PATCH] Improvements and Docker --- .env | 12 ++++ FlightMicroservice/Dockerfile | 2 - FlightMicroservice/FlightMicroservice.csproj | 2 +- FlightMicroservice/Models/Seat.cs | 4 +- FlightMicroservice/Program.cs | 26 +++++++-- FlightMicroservice/Services/SeatService.cs | 2 +- FlightMicroservice/appsettings.json | 4 +- UserMicroservice/.dockerignore | 30 ++++++++++ .../Controllers/UserController.cs | 56 +++++++++++++++---- UserMicroservice/Dockerfile | 23 ++++++++ UserMicroservice/Models/RegisterModel.cs | 1 + UserMicroservice/Models/User.cs | 24 +++++--- UserMicroservice/Program.cs | 2 + UserMicroservice/Services/AuthService.cs | 15 +++-- UserMicroservice/Services/IAuthService.cs | 2 +- UserMicroservice/Services/IUserService.cs | 3 +- UserMicroservice/Services/UserService.cs | 42 ++++++++++---- UserMicroservice/UserMicroservice.csproj | 4 +- docker-compose.yml | 41 ++++++++++++++ 19 files changed, 242 insertions(+), 53 deletions(-) create mode 100644 .env create mode 100644 UserMicroservice/.dockerignore create mode 100644 UserMicroservice/Dockerfile create mode 100644 docker-compose.yml diff --git a/.env b/.env new file mode 100644 index 0000000..09923e3 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# Image settings +IMAGE_TAG=0.0.1 + +# Database configuration +DB_PORT=3308 +DB_NAME=AspNetCoreDb +DB_USER=root +DB_PASSWORD= +DB_CHARSET=utf8mb4 + +# Service ports +USER_MICROSERVICE_PORT=5089 # This port will be used as the JWT issuer diff --git a/FlightMicroservice/Dockerfile b/FlightMicroservice/Dockerfile index 9c6a362..8038750 100644 --- a/FlightMicroservice/Dockerfile +++ b/FlightMicroservice/Dockerfile @@ -1,5 +1,3 @@ -#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 diff --git a/FlightMicroservice/FlightMicroservice.csproj b/FlightMicroservice/FlightMicroservice.csproj index dc33c0f..8b66623 100644 --- a/FlightMicroservice/FlightMicroservice.csproj +++ b/FlightMicroservice/FlightMicroservice.csproj @@ -11,7 +11,7 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.2" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> diff --git a/FlightMicroservice/Models/Seat.cs b/FlightMicroservice/Models/Seat.cs index e8472d3..05391ae 100644 --- a/FlightMicroservice/Models/Seat.cs +++ b/FlightMicroservice/Models/Seat.cs @@ -1,4 +1,6 @@ -namespace FlightMicroservice.Models +using System.Text.Json.Serialization; + +namespace FlightMicroservice.Models { public class Seat { diff --git a/FlightMicroservice/Program.cs b/FlightMicroservice/Program.cs index 5b9d978..82fabff 100644 --- a/FlightMicroservice/Program.cs +++ b/FlightMicroservice/Program.cs @@ -4,13 +4,15 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.Text; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; +}); -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -32,7 +34,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) }; }); -// Add dependency injections +// Dependency injections builder.Services.AddScoped<IFlightService, FlightService>(); builder.Services.AddScoped<ISeatService, SeatService>(); @@ -47,6 +49,22 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +// Middleware for handling the access token in cookies +app.Use(async (context, next) => +{ + var accessToken = context.Request.Cookies["AccessToken"]; + if (!string.IsNullOrEmpty(accessToken)) + { + if (!context.Request.Headers.ContainsKey("Authorization")) + { + context.Request.Headers.Append("Authorization", "Bearer " + accessToken); + } + } + + await next(); +}); + +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/FlightMicroservice/Services/SeatService.cs b/FlightMicroservice/Services/SeatService.cs index 9a760d8..c3f3618 100644 --- a/FlightMicroservice/Services/SeatService.cs +++ b/FlightMicroservice/Services/SeatService.cs @@ -29,7 +29,7 @@ namespace FlightMicroservice.Services int affectedRows = dbContext.Seats .Where(seat => seat.Id == id) .ExecuteUpdate(setters => setters - .SetProperty(seat => seat.IsAvailable, true)); + .SetProperty(seat => seat.IsAvailable, false)); dbContext.SaveChanges(); diff --git a/FlightMicroservice/appsettings.json b/FlightMicroservice/appsettings.json index 8ffdd21..189dc7f 100644 --- a/FlightMicroservice/appsettings.json +++ b/FlightMicroservice/appsettings.json @@ -11,7 +11,7 @@ }, "Jwt": { "Key": "0QTrd3jToEYj205k01A2R87Hc5YpqDNeywg7JzQpczs=", - "Issuer": "http://localhost:5175", - "Audience": "http://localhost:5175" + "Issuer": "http://localhost:5089", + "Audience": "http://localhost:5089" } } diff --git a/UserMicroservice/.dockerignore b/UserMicroservice/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/UserMicroservice/.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/UserMicroservice/Controllers/UserController.cs b/UserMicroservice/Controllers/UserController.cs index 5db458a..ea86b63 100644 --- a/UserMicroservice/Controllers/UserController.cs +++ b/UserMicroservice/Controllers/UserController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; using UserMicroservice.Models; using UserMicroservice.Services; @@ -20,18 +21,46 @@ namespace UserMicroservice.Controllers #region Auth Endpoints - // POST: api/Users/register + // POST: api/User/register [HttpPost("register")] public IActionResult Register([FromBody] RegisterModel model) { - User user = _userService.CreateUser(model.Email, model.Username, model.Password); - if(user == null) + try + { + User user = _userService.CreateUser(model.Email, model.Username, model.Password, model.UserType); + if (user == null) + return BadRequest(); + + return authenticateUser(user.Id); + + } catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + } + + // POST: api/User/authorize + [HttpPost("authorize")] + public IActionResult Authorize() + { + string? refreshToken = Request.Cookies["RefreshToken"]; + if (string.IsNullOrEmpty(refreshToken)) + return BadRequest("Refresh token is missing."); + + if (!_authService.ValidateRefreshToken(refreshToken)) + return Unauthorized("Invalid or expired refresh token."); + + string? userIdString = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; + if(userIdString == null) return BadRequest(); - return authenticateUser(user); + if (!int.TryParse(userIdString, out int userId)) + return BadRequest("User ID is invalid."); + + return authenticateUser(userId); } - // POST: api/Users/login + // POST: api/User/login [HttpPost("login")] public IActionResult Login([FromBody] LoginModel model) { @@ -39,12 +68,12 @@ namespace UserMicroservice.Controllers if(user == null) return Unauthorized(); - return authenticateUser(user); + return authenticateUser(user.Id); } - private IActionResult authenticateUser(User user) + private IActionResult authenticateUser(int userId) { - AuthTokenPair authToken = _authService.AuthenticateUser(user); + AuthTokenPair authToken = _authService.AuthenticateUser(userId); if (authToken == null) return BadRequest(); @@ -69,14 +98,17 @@ namespace UserMicroservice.Controllers return Ok(); } - // POST: api/Users/logout + // POST: api/User/logout [Authorize] [HttpPost("logout")] public IActionResult Logout() { string? refreshToken = Request.Cookies["RefreshToken"]; if(string.IsNullOrEmpty(refreshToken)) - return BadRequest(); + return BadRequest("Refresh token is missing."); + + if (!_authService.ValidateRefreshToken(refreshToken)) + return Unauthorized("Invalid or expired refresh token."); _authService.RevokeRefreshToken(refreshToken); @@ -101,7 +133,7 @@ namespace UserMicroservice.Controllers #endregion - // GET: api/Users + // GET: api/User [Authorize] [HttpGet()] public IActionResult GetUsers() @@ -113,7 +145,7 @@ namespace UserMicroservice.Controllers return Ok(users); } - // GET: api/Users/{id} + // GET: api/User/{id} [Authorize] [HttpGet("{id}")] public IActionResult GetUser(int id) diff --git a/UserMicroservice/Dockerfile b/UserMicroservice/Dockerfile new file mode 100644 index 0000000..baab33d --- /dev/null +++ b/UserMicroservice/Dockerfile @@ -0,0 +1,23 @@ +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 ["UserMicroservice.csproj", "."] +RUN dotnet restore "./UserMicroservice.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "./UserMicroservice.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./UserMicroservice.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "UserMicroservice.dll"] \ No newline at end of file diff --git a/UserMicroservice/Models/RegisterModel.cs b/UserMicroservice/Models/RegisterModel.cs index 5634559..8c2e180 100644 --- a/UserMicroservice/Models/RegisterModel.cs +++ b/UserMicroservice/Models/RegisterModel.cs @@ -5,5 +5,6 @@ public required string Username { get; set; } public required string Email { get; set; } public required string Password { get; set; } + public required UserType UserType { get; set; } } } diff --git a/UserMicroservice/Models/User.cs b/UserMicroservice/Models/User.cs index cca8b8e..0d113d6 100644 --- a/UserMicroservice/Models/User.cs +++ b/UserMicroservice/Models/User.cs @@ -1,22 +1,30 @@ -namespace UserMicroservice.Models +using Microsoft.AspNetCore.Identity; + +namespace UserMicroservice.Models { public class User { // Parameterless constructor for EF Core public User() { } - public User(string userName, string email, string passwordHash) + public User(string userName, string email, UserType userType) { Username = userName; Email = email; - PasswordHash = passwordHash; + Type = userType; + } + + public int Id { get; private set; } + public string Username { get; private set; } + public string Email { get; private set; } + public string PasswordHash { get; private set; } + public UserType Type { get; private set; } = UserType.CUSTOMER; + + public void SetPasswordHash(IPasswordHasher<User> passwordHasher, string password) + { + PasswordHash = passwordHasher.HashPassword(this, password); } - public int Id { get; internal set; } - public string Username { get; internal set; } - public string Email { get; internal set; } - public string PasswordHash { get; internal set; } - public UserType Type { get; internal set; } = UserType.CUSTOMER; } public enum UserType diff --git a/UserMicroservice/Program.cs b/UserMicroservice/Program.cs index 34f4e5e..cbfa241 100644 --- a/UserMicroservice/Program.cs +++ b/UserMicroservice/Program.cs @@ -4,6 +4,7 @@ using UserMicroservice.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; +using Microsoft.AspNetCore.Identity; var builder = WebApplication.CreateBuilder(args); @@ -32,6 +33,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) }); // Add dependency injections +builder.Services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>(); builder.Services.AddScoped<IAuthService, AuthService>(); builder.Services.AddScoped<IUserService, UserService>(); diff --git a/UserMicroservice/Services/AuthService.cs b/UserMicroservice/Services/AuthService.cs index b8cc84e..36f144c 100644 --- a/UserMicroservice/Services/AuthService.cs +++ b/UserMicroservice/Services/AuthService.cs @@ -20,15 +20,15 @@ namespace UserMicroservice.Services _context = applicationDbContext; } - public AuthTokenPair AuthenticateUser(User user) + public AuthTokenPair AuthenticateUser(int userId) { - string accessToken = GenerateAccessToken(user); - string refreshToken = GenerateRefreshToken(user.Id).Token; + string accessToken = GenerateAccessToken(userId); + string refreshToken = GenerateRefreshToken(userId).Token; return new AuthTokenPair(accessToken, refreshToken); } - private string GenerateAccessToken(User user) + private string GenerateAccessToken(int userId) { string? configuredKey = _configuration["Jwt:Key"]; string? configuredIssuer = _configuration["Jwt:Issuer"]; @@ -41,9 +41,8 @@ namespace UserMicroservice.Services var claims = new List<Claim> { - new Claim(JwtRegisteredClaimNames.Sub, user.Username), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.NameIdentifier, userId.ToString()), }; var token = new JwtSecurityToken( @@ -68,8 +67,8 @@ namespace UserMicroservice.Services public bool ValidateRefreshToken(string token) { - RefreshToken refreshToken = _context.RefreshTokens.Single(t => t.Token == token); - if (refreshToken != null) { } + RefreshToken? refreshToken = _context.RefreshTokens.SingleOrDefault(t => t.Token == token); + if (refreshToken != null) { if (DateTime.UtcNow <= refreshToken.ExpirationDate) { diff --git a/UserMicroservice/Services/IAuthService.cs b/UserMicroservice/Services/IAuthService.cs index 4f30e06..26bff1f 100644 --- a/UserMicroservice/Services/IAuthService.cs +++ b/UserMicroservice/Services/IAuthService.cs @@ -4,7 +4,7 @@ namespace UserMicroservice.Services { public interface IAuthService { - AuthTokenPair AuthenticateUser(User user); + AuthTokenPair AuthenticateUser(int userId); void RevokeRefreshToken(string token); diff --git a/UserMicroservice/Services/IUserService.cs b/UserMicroservice/Services/IUserService.cs index b780468..2a4e75b 100644 --- a/UserMicroservice/Services/IUserService.cs +++ b/UserMicroservice/Services/IUserService.cs @@ -9,8 +9,9 @@ namespace UserMicroservice.Services User? GetUser(int userId); User? GetUser(string username, string password); List<User> GetUsers(); - User CreateUser(string email, string userName, string password); + User CreateUser(string email, string userName, string password, UserType UserType); User UpdateUser(User updatedUser); bool DeleteUser(string username); + User? GetUserByEmail(string email); } } diff --git a/UserMicroservice/Services/UserService.cs b/UserMicroservice/Services/UserService.cs index 40a3c13..9288ae8 100644 --- a/UserMicroservice/Services/UserService.cs +++ b/UserMicroservice/Services/UserService.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using UserMicroservice.Models; namespace UserMicroservice.Services @@ -6,16 +7,18 @@ namespace UserMicroservice.Services public class UserService : IUserService { private readonly ApplicationDbContext _context; + private readonly IPasswordHasher<User> _passwordHasher; - public UserService(ApplicationDbContext context) + public UserService(ApplicationDbContext context, IPasswordHasher<User> passwordHasher) { _context = context; + _passwordHasher = passwordHasher; } public bool DeleteUser(string username) { User? user = _context.Users.SingleOrDefault(user => user.Username == username); - if(user == null) + if (user == null) return false; _context.Users.Remove(user); @@ -24,8 +27,8 @@ namespace UserMicroservice.Services return true; } - public List<User> GetUsers() - { + public List<User> GetUsers() + { List<User> users = _context.Users.ToList(); return users; } @@ -38,20 +41,39 @@ namespace UserMicroservice.Services public User? GetUser(int userId) { - User? user = _context.Users.SingleOrDefault(user=> user.Id == userId); + User? user = _context.Users.SingleOrDefault(user => user.Id == userId); return user; } public User? GetUser(string username, string password) { - User? user = _context.Users - .SingleOrDefault(user => user.Username == username && user.PasswordHash == password); + User? user = GetUser(username); + + if (user != null) + { + // Verify the hashed password with the provided password + var verificationResult = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); + if (verificationResult == PasswordVerificationResult.Success) + return user; // Password is correct, return the user + } + + return null; // User not found or password does not match + } + + public User? GetUserByEmail(string email) + { + User? user = _context.Users.SingleOrDefault(user => user.Email == email); return user; } - public User CreateUser(string email, string userName, string password) + public User CreateUser(string email, string userName, string password, UserType userType) { - User user = new User(userName, email, password); + if (GetUser(userName) != null || GetUserByEmail(email) != null) + throw new InvalidOperationException($"A User with the provided credntials already exists"); + + User user = new User(userName, email, userType); + user.SetPasswordHash(_passwordHasher, password); + _context.Users.Add(user); _context.SaveChanges(); diff --git a/UserMicroservice/UserMicroservice.csproj b/UserMicroservice/UserMicroservice.csproj index 3aed6c1..586ea39 100644 --- a/UserMicroservice/UserMicroservice.csproj +++ b/UserMicroservice/UserMicroservice.csproj @@ -8,11 +8,11 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> - - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2" PrivateAssets="All" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2" PrivateAssets="All" /> </ItemGroup> </Project> diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..83dca10 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + usermicroservice: + build: + context: ./UserMicroservice + image: usermicroservice:${IMAGE_TAG} + ports: + - "${USER_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} + + flightmicroservice: + build: + context: ./FlightMicroservice + image: flightmicroservice:${IMAGE_TAG} + ports: + - "5175: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} + + client: + build: + context: ./client + image: client:${IMAGE_TAG} + ports: + - "4200:4200" + + + +# ... other services -- GitLab