From c7dbb436f5c725e9af55775489b18af1f9abb80b Mon Sep 17 00:00:00 2001
From: Mouaz Abdelsamad <ma03081@surrey.ac.uk>
Date: Tue, 2 Apr 2024 00:56:50 +0100
Subject: [PATCH] Dockerize Gateway and improvments

---
 .env                                          | 15 ++++---
 Database/InitTables.sql                       | 34 ++++++++++++++
 .../Clients/UserService/IUserServiceClient.cs |  1 +
 .../Clients/UserService/UserServiceClient.cs  |  5 +++
 GatewayAPI/Controllers/UserController.cs      |  8 ++++
 GatewayAPI/Models/UserUpdateInfo.cs           |  9 ++++
 GatewayAPI/Program.cs                         |  2 +-
 GatewayAPI/ServiceCollectionExtensions.cs     |  8 ++--
 GatewayAPI/appsettings.json                   |  4 +-
 .../Controllers/UserController.cs             | 44 ++++++++++++++-----
 UserMicroservice/Models/UpdateUserModel.cs    |  9 ++++
 UserMicroservice/Services/IUserService.cs     |  2 +-
 UserMicroservice/Services/UserService.cs      | 27 +++++++++---
 docker-compose.yml                            | 41 ++++++++++++-----
 14 files changed, 169 insertions(+), 40 deletions(-)
 create mode 100644 Database/InitTables.sql
 create mode 100644 GatewayAPI/Models/UserUpdateInfo.cs
 create mode 100644 UserMicroservice/Models/UpdateUserModel.cs

diff --git a/.env b/.env
index 09923e3..673e590 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 0000000..6805d23
--- /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/Clients/UserService/IUserServiceClient.cs b/GatewayAPI/Clients/UserService/IUserServiceClient.cs
index b37273f..dfcaeb7 100644
--- a/GatewayAPI/Clients/UserService/IUserServiceClient.cs
+++ b/GatewayAPI/Clients/UserService/IUserServiceClient.cs
@@ -10,5 +10,6 @@ namespace GatewayAPI.Clients.UserService
         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
index e6533bd..46752a4 100644
--- a/GatewayAPI/Clients/UserService/UserServiceClient.cs
+++ b/GatewayAPI/Clients/UserService/UserServiceClient.cs
@@ -45,5 +45,10 @@ namespace GatewayAPI.Clients.UserService
             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/UserController.cs b/GatewayAPI/Controllers/UserController.cs
index 2971951..2061f69 100644
--- a/GatewayAPI/Controllers/UserController.cs
+++ b/GatewayAPI/Controllers/UserController.cs
@@ -56,5 +56,13 @@ namespace GatewayAPI.Controllers
             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/Models/UserUpdateInfo.cs b/GatewayAPI/Models/UserUpdateInfo.cs
new file mode 100644
index 0000000..bf4c9c3
--- /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
index 6f900f8..8f7722b 100644
--- a/GatewayAPI/Program.cs
+++ b/GatewayAPI/Program.cs
@@ -21,7 +21,7 @@ if (app.Environment.IsDevelopment())
     app.UseSwaggerUI();
 }
 
-app.UseHttpsRedirection();
+//app.UseHttpsRedirection();
 app.UseAuthorization();
 app.MapControllers();
 app.Run();
diff --git a/GatewayAPI/ServiceCollectionExtensions.cs b/GatewayAPI/ServiceCollectionExtensions.cs
index b928371..7235c81 100644
--- a/GatewayAPI/ServiceCollectionExtensions.cs
+++ b/GatewayAPI/ServiceCollectionExtensions.cs
@@ -10,14 +10,14 @@ namespace GatewayAPI
         {
             services.AddHttpClient<IUserServiceClient, UserServiceClient>(client =>
             {
-                string? baseUrl = configurationManager["UserMicroservice:BaseUrl"];
-                client.BaseAddress = new Uri(baseUrl ?? throw new InvalidOperationException("UserMicroservice BaseUrl is not configured."));
+                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"];
-                client.BaseAddress = new Uri(baseUrl ?? throw new InvalidOperationException("FlightMicroservice BaseUrl is not configured."));
+                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.json b/GatewayAPI/appsettings.json
index 0345e2e..f0c8836 100644
--- a/GatewayAPI/appsettings.json
+++ b/GatewayAPI/appsettings.json
@@ -7,9 +7,9 @@
   },
   "AllowedHosts": "*",
   "UserMicroservice": {
-    "BaseUrl": "http://localhost:5089/"
+    "BaseUrl": "http://localhost:5089"
   },
   "FlightMicroservice": {
-    "BaseUrl": "http://localhost:5175/"
+    "BaseUrl": "http://localhost:5175"
   }
 }
diff --git a/UserMicroservice/Controllers/UserController.cs b/UserMicroservice/Controllers/UserController.cs
index ea86b63..b14e108 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 0000000..bda556a
--- /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 2a4e75b..0e6deff 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 9288ae8..59c0f37 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 83dca10..109ae71 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"
-- 
GitLab