diff --git a/.gitignore b/.gitignore index add57be..c39627f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ + + +do-not-commit.txt \ No newline at end of file diff --git a/.idea/.idea.SecureDesignProject/.idea/dataSources.xml b/.idea/.idea.SecureDesignProject/.idea/dataSources.xml new file mode 100644 index 0000000..3d1eb39 --- /dev/null +++ b/.idea/.idea.SecureDesignProject/.idea/dataSources.xml @@ -0,0 +1,18 @@ + + + + + sqlserver.jb + true + com.jetbrains.jdbc.sqlserver.SqlServerDriver + Server=localhost,1433;Database=master + + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/.idea.SecureDesignProject/.idea/sqldialects.xml b/.idea/.idea.SecureDesignProject/.idea/sqldialects.xml new file mode 100644 index 0000000..1057460 --- /dev/null +++ b/.idea/.idea.SecureDesignProject/.idea/sqldialects.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SecureDesignProject/.idea/vcs.xml b/.idea/.idea.SecureDesignProject/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.SecureDesignProject/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs index c4d28aa..267a6cd 100644 --- a/Controllers/AuthController.cs +++ b/Controllers/AuthController.cs @@ -7,8 +7,17 @@ namespace SecureDesignProject.Controllers; +/// +/// Controller responsible for handling authentication-related actions such as login, registration, and logout. +/// public class AuthController(ILogger logger, AuthService authService) : Controller { + + /// + /// Displays the login page to the user. + /// + /// A ViewResult displaying the login page. + public IActionResult Login() { TempData["hideLogout"] = true; @@ -16,6 +25,10 @@ public IActionResult Login() return View(); } + /// + /// Displays the registration page to the user. + /// + /// A ViewResult displaying the registration page. public IActionResult Register() { TempData["hideLogout"] = true; @@ -23,6 +36,10 @@ public IActionResult Register() return View(); } + /// + /// Logs out the user by invalidating their session and removing the session cookie. + /// + /// Redirects to the Login Page. public IActionResult Logout() { var sessionCookie = Request.GetSessionCookie(); @@ -39,6 +56,13 @@ public IActionResult Logout() return RedirectToAction("Login", "Auth"); } + /// + /// Handles the login request by validating credentials and setting a session cookie upon success. + /// + /// The login details submitted by the user (email and password). + /// + /// On success, redirects to the home page; otherwise, redisplays the login page with an error message. + /// [HttpPost] public IActionResult Login([FromForm] LoginDetails loginDetails) { @@ -57,6 +81,13 @@ public IActionResult Login([FromForm] LoginDetails loginDetails) return RedirectToAction("Index", "Home"); } + /// + /// Handles the registration request by creating a new account and logging the user in upon success. + /// + /// The registration details submitted by the user. + /// + /// On success, redirects to the home page; otherwise, redisplays the registration page with an error message. + /// [HttpPost] public IActionResult Register([FromForm] RegisterDetails registerDetails) { @@ -75,6 +106,10 @@ public IActionResult Register([FromForm] RegisterDetails registerDetails) return RedirectToAction("Index", "Home"); } + /// + /// Displays an error page for unhandled exceptions or issues. + /// + /// A ViewResult displaying the error page with error details. [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { diff --git a/Controllers/DashboardController.cs b/Controllers/DashboardController.cs index d1d3a8f..6ef4032 100644 --- a/Controllers/DashboardController.cs +++ b/Controllers/DashboardController.cs @@ -5,8 +5,15 @@ namespace SecureDesignProject.Controllers; +/// +/// Controller responsible for handling dashboard functionality for patients and caregivers. +/// public class DashboardController(PatientService patientService, CaregiverService caregiverService) : Controller { + /// + /// Displays patient-specific dashboard information. + /// + /// A ViewResult containing patient information or redirects to an error page if not found. public IActionResult Patient() { var patient = patientService.GetPatientInfoByAccountId(HttpContext.GetAccountId()); @@ -17,6 +24,10 @@ public IActionResult Patient() return View(patient); } + /// + /// Displays caregiver-specific dashboard information. + /// + /// A ViewResult containing caregiver overview or redirects to an error page if not found. public IActionResult Caregiver() { var caregiverOverview = caregiverService.GetCaregiverOverview(HttpContext.GetAccountId()); @@ -27,6 +38,11 @@ public IActionResult Caregiver() return View(caregiverOverview); } + /// + /// Displays patient information for a specific patient, intended for caregiver view. + /// + /// The unique identifier of the patient. + /// A ViewResult containing the patient's information. [Route("dashboard/caregiverView/{patientId:guid}")] public IActionResult CaregiverView([FromRoute] Guid patientId) { @@ -35,12 +51,23 @@ public IActionResult CaregiverView([FromRoute] Guid patientId) return View(patientInfo); } + /// + /// Displays the update address form for the logged-in patient. + /// + /// A ViewResult containing the patient's address. public IActionResult UpdateAddress() { var address = patientService.GetAddressByAccountId(HttpContext.GetAccountId()); return View(address); } + /// + /// Processes an update address request for the logged-in patient. + /// + /// The updated address information. + /// + /// Redirects to the patient dashboard if successful; otherwise, redisplays the form with an error message. + /// [HttpPost] public IActionResult UpdateAddress(Address address) { @@ -55,6 +82,11 @@ public IActionResult UpdateAddress(Address address) return RedirectToAction("Patient"); } + /// + /// Displays the update appointment form for a specific appointment. + /// + /// The unique identifier of the appointment. + /// A ViewResult containing appointment information. [Route("dashboard/updateAppointment/{appointmentId:guid}")] public IActionResult UpdateAppointment([FromRoute] Guid appointmentId) { @@ -64,6 +96,11 @@ public IActionResult UpdateAppointment([FromRoute] Guid appointmentId) } + /// + /// Creates a new appointment record and redirects to the update form. + /// + /// The unique identifier of the patient. + /// A RedirectToRouteResult pointing to the update appointment form. [Route("dashboard/updateAppointment/new/{patientId:guid}")] public IActionResult NewAppointment([FromRoute] Guid patientId) { @@ -72,6 +109,14 @@ public IActionResult NewAppointment([FromRoute] Guid patientId) } + /// + /// Processes an update appointment request. + /// + /// The unique identifier of the appointment being updated. + /// The updated appointment information. + /// + /// Redirects to the caregiver dashboard if successful; otherwise, redisplays the form with an error message. + /// [HttpPost] [Route("dashboard/updateAppointment/{appointmentId:guid}")] public IActionResult UpdateAppointment([FromRoute] Guid appointmentId, AppointmentInfo appointment) @@ -87,6 +132,11 @@ public IActionResult UpdateAppointment([FromRoute] Guid appointmentId, Appointme return RedirectToAction("Caregiver"); } + /// + /// Displays the update record form for a specific patient record. + /// + /// The unique identifier of the record. + /// A ViewResult containing the patient record. [Route("dashboard/updateRecord/{recordId:guid}")] public IActionResult UpdateRecord([FromRoute] Guid recordId) { @@ -96,6 +146,11 @@ public IActionResult UpdateRecord([FromRoute] Guid recordId) } + /// + /// Creates a new patient record and redirects to the update form. + /// + /// The unique identifier of the patient. + /// A RedirectToRouteResult pointing to the update record form. [Route("dashboard/updateRecord/new/{patientId:guid}")] public IActionResult NewRecord([FromRoute] Guid patientId) { @@ -103,6 +158,14 @@ public IActionResult NewRecord([FromRoute] Guid patientId) return RedirectToRoute($"Dashboard/UpdateAppointment/{newId}"); } + /// + /// Processes an update record request. + /// + /// The unique identifier of the record being updated. + /// The updated patient record information. + /// + /// Redirects to the caregiver dashboard if successful; otherwise, redisplays the form with an error message. + /// [HttpPost] [Route("dashboard/updateRecord/{recordId:guid}")] public IActionResult UpdateRecord([FromRoute] Guid recordId, PatientRecord record) diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index 41184ab..81c9e18 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -6,9 +6,17 @@ namespace SecureDesignProject.Controllers; +/// +/// The `HomeController` class handles navigation to the appropriate dashboard based on the user's account type. +/// public class HomeController(ILogger logger, DatabaseService dbService) : Controller { - // Redirect to relevant dashboard depending on user type + /// + /// Determines the user's account type based on the session cookie and redirects to the appropriate dashboard. + /// + /// + /// A redirect to the patient's or caregiver's dashboard, or to the login page if the session is invalid. + /// public IActionResult Index() { var sessionKey = Request.GetSessionCookie(); @@ -25,11 +33,11 @@ public IActionResult Index() }; } - public IActionResult Privacy() - { - return View(); - } + /// + /// Displays an error page when an exception or invalid state occurs. + /// + /// An `ErrorViewModel` with the current request's ID. [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { diff --git a/Extensions/HttpExtensions.cs b/Extensions/HttpExtensions.cs index 04a20fb..02b5555 100644 --- a/Extensions/HttpExtensions.cs +++ b/Extensions/HttpExtensions.cs @@ -1,7 +1,18 @@ namespace SecureDesignProject.Extensions; + +/// +/// A static class providing extension methods for handling HTTP-related functionality +/// such as cookies and retrieving account identifiers from the HTTP context. +/// public static class HttpExtensions { + /// + /// Sets a secure, HTTP-only cookie named "session_key" containing the session key as a hex string. + /// + /// The HTTP response object to set the cookie on. + /// The session key to be stored in the cookie. + public static void SetSessionKeyCookie(this HttpResponse response, byte[] sessionKey) { var options = new CookieOptions @@ -13,12 +24,23 @@ public static void SetSessionKeyCookie(this HttpResponse response, byte[] sessio } + + /// + /// Retrieves the "session_key" cookie from the HTTP request, if it exists, and converts it to a byte array. + /// + /// The HTTP request object containing the cookies. + /// The session key as a byte array, or null if the cookie is not present. public static byte[]? GetSessionCookie(this HttpRequest request) { var cookie = request.Cookies["session_key"]; return cookie == null ? null : Convert.FromHexString(cookie!); } + /// + /// Retrieves the account ID stored in the HTTP context's items collection. + /// + /// The HTTP context object containing the account ID. + /// The account ID as a GUID, or Guid.Empty if not found. public static Guid GetAccountId(this HttpContext context) { return context.Items["accountId"] as Guid? ?? Guid.Empty; diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs index 9bba24b..7886ab5 100644 --- a/Extensions/StringExtensions.cs +++ b/Extensions/StringExtensions.cs @@ -1,5 +1,8 @@ namespace SecureDesignProject.Extensions; +/// +/// A static class providing extension methods for string related functionality +/// public static class StringExtensions { public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value); diff --git a/Migrations/1_create_db.sql b/Migrations/1_create_db.sql index d66078e..2927005 100644 --- a/Migrations/1_create_db.sql +++ b/Migrations/1_create_db.sql @@ -3,92 +3,94 @@ CREATE DATABASE HealthServiceDb GO + -- Create Tables CREATE TABLE Accounts( - AccountId UNIQUEIDENTIFIER NOT NULL, - Email NVARCHAR(MAX) NOT NULL UNIQUE, - HashedPassword BINARY(256) NOT NULL, - - PRIMARY KEY (AccountId) + AccountId UNIQUEIDENTIFIER NOT NULL, + Email NVARCHAR(2048) NOT NULL UNIQUE, + HashedPassword BINARY(256) NOT NULL, + + PRIMARY KEY (AccountId) ); CREATE TABLE Patients( - PatientId UNIQUEIDENTIFIER NOT NULL, - FirstName NVARCHAR(255) NOT NULL, - LastName NVARCHAR(255) NOT NULL, - Address NVARCHAR(MAX), - - AccountId UNIQUEIDENTIFIER NOT NULL, - - PRIMARY KEY (PatientId), - FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId) + PatientId UNIQUEIDENTIFIER NOT NULL, + FirstName NVARCHAR(256) NOT NULL, + LastName NVARCHAR(256) NOT NULL, + Address NVARCHAR(2048), + + AccountId UNIQUEIDENTIFIER NOT NULL, + + PRIMARY KEY (PatientId), + FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId) ); CREATE TABLE Caregivers( - CaregiverId UNIQUEIDENTIFIER NOT NULL, - FirstName NVARCHAR(255) NOT NULL, - LastName NVARCHAR(255) NOT NULL, + CaregiverId UNIQUEIDENTIFIER NOT NULL, + FirstName NVARCHAR(256) NOT NULL, + LastName NVARCHAR(256) NOT NULL, - AccountId UNIQUEIDENTIFIER NOT NULL, + AccountId UNIQUEIDENTIFIER NOT NULL, - PRIMARY KEY (CaregiverId), - FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId) + PRIMARY KEY (CaregiverId), + FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId) ); CREATE TABLE Caregiver_Patients( - PatientId UNIQUEIDENTIFIER NOT NULL, - CaregiverId UNIQUEIDENTIFIER NOT NULL, - - CurrentlyAssigned Bit NOT NULL, - AssignedAt DATETIME NOT NULL, - UnassignedAt DATETIME, + PatientId UNIQUEIDENTIFIER NOT NULL, + CaregiverId UNIQUEIDENTIFIER NOT NULL, - FOREIGN KEY (PatientId) REFERENCES Patients(PatientId), - FOREIGN KEY (CaregiverId) REFERENCES Caregivers(CaregiverId) + CurrentlyAssigned Bit NOT NULL, + AssignedAt DATETIME NOT NULL, + UnassignedAt DATETIME, + + FOREIGN KEY (PatientId) REFERENCES Patients(PatientId), + FOREIGN KEY (CaregiverId) REFERENCES Caregivers(CaregiverId) ); CREATE TABLE PatientsRecords( RecordId UNIQUEIDENTIFIER NOT NULL, - + PatientId UNIQUEIDENTIFIER NOT NULL, CreatorId UNIQUEIDENTIFIER NOT NULL, - RecordName NVARCHAR(MAX), - RecordData NVARCHAR(MAX), - + RecordName NVARCHAR(256), + RecordData NVARCHAR(2048), + PRIMARY KEY (RecordId), FOREIGN KEY (PatientId) REFERENCES Patients(PatientId), FOREIGN KEY (CreatorId) REFERENCES Caregivers(CaregiverId) ); CREATE TABLE Appointments( - AppointmentId UNIQUEIDENTIFIER NOT NULL, + AppointmentId UNIQUEIDENTIFIER NOT NULL, - PatientId UNIQUEIDENTIFIER NOT NULL, - CreatorId UNIQUEIDENTIFIER NOT NULL, + PatientId UNIQUEIDENTIFIER NOT NULL, + CreatorId UNIQUEIDENTIFIER NOT NULL, - AppointmentTime DATETIME NOT NULL, - Duration DATETIMEOFFSET NOT NULL, - Notes NVARCHAR(MAX), + AppointmentTime DATETIME NOT NULL, + Duration DATETIMEOFFSET NOT NULL, + Notes NVARCHAR(2048), - PRIMARY KEY (AppointmentId), - FOREIGN KEY (PatientId) REFERENCES Patients(PatientId), - FOREIGN KEY (CreatorId) REFERENCES Caregivers(CaregiverId) + PRIMARY KEY (AppointmentId), + FOREIGN KEY (PatientId) REFERENCES Patients(PatientId), + FOREIGN KEY (CreatorId) REFERENCES Caregivers(CaregiverId) ); CREATE TABLE Sessions( - SessionId UNIQUEIDENTIFIER NOT NULL, - AccountId UNIQUEIDENTIFIER NOT NULL, - - SessionKey BINARY(256) NOT NULL, - IsValid BIT NOT NULL, - - Created DATETIME NOT NULL, - LastSeen DATETIME NOT NULL, - Expires DATETIME NOT NULL, - - PRIMARY KEY (SessionId), - FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId) + SessionId UNIQUEIDENTIFIER NOT NULL, + AccountId UNIQUEIDENTIFIER NOT NULL, + + SessionKey BINARY(256) NOT NULL, + IsValid BIT NOT NULL, + + Created DATETIME NOT NULL, + LastSeen DATETIME NOT NULL, + Expires DATETIME NOT NULL, + + PRIMARY KEY (SessionId), + FOREIGN KEY (AccountId) REFERENCES Accounts(AccountId) ); GO + diff --git a/Migrations/2_add_seperate_salt_col.sql b/Migrations/2_add_seperate_salt_col.sql index 7f4cb31..62ad2bb 100644 --- a/Migrations/2_add_seperate_salt_col.sql +++ b/Migrations/2_add_seperate_salt_col.sql @@ -1,2 +1,2 @@ ALTER TABLE Accounts -ADD Salt BINARY(128) NOT NULL DEFAULT 123; \ No newline at end of file +ADD Salt BINARY(128) NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/Models/AuthModels.cs b/Models/AuthModels.cs index d311b76..553341c 100644 --- a/Models/AuthModels.cs +++ b/Models/AuthModels.cs @@ -31,9 +31,6 @@ public record RegisterDetails [Required] [MaxLength(255)] public string LastName { get; init; } = ""; - - [Required] - public int InviteCode { get; init; } }; public enum UserType diff --git a/Program.cs b/Program.cs index 349e466..3893a18 100644 --- a/Program.cs +++ b/Program.cs @@ -4,9 +4,11 @@ var builder = WebApplication.CreateBuilder(args); +var connectionString = builder.Configuration.GetConnectionString("Database") ?? ""; + // Add services to the container. builder.Services.AddControllersWithViews(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(new DatabaseService(connectionString)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/SecureDesignProject.csproj b/SecureDesignProject.csproj index 22acc0c..9470c18 100644 --- a/SecureDesignProject.csproj +++ b/SecureDesignProject.csproj @@ -14,4 +14,16 @@ + + + ..\..\.nuget\packages\moq\4.20.72\lib\net6.0\Moq.dll + + + ..\..\.nuget\packages\xunit.assert\2.9.2\lib\net6.0\xunit.assert.dll + + + ..\..\.nuget\packages\xunit.extensibility.core\2.9.2\lib\netstandard1.1\xunit.core.dll + + + diff --git a/SecureDesignProject.sln b/SecureDesignProject.sln index 96e0ef7..8a52d05 100644 --- a/SecureDesignProject.sln +++ b/SecureDesignProject.sln @@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution compose.yaml = compose.yaml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureDesignProjectTests", "SecureDesignProjectTests\SecureDesignProjectTests.csproj", "{DCC98D37-7666-4880-9142-2103D87A6C35}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,5 +19,9 @@ Global {46765F06-2B8D-47A4-A8D4-8961DA4BC3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {46765F06-2B8D-47A4-A8D4-8961DA4BC3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {46765F06-2B8D-47A4-A8D4-8961DA4BC3E9}.Release|Any CPU.Build.0 = Release|Any CPU + {DCC98D37-7666-4880-9142-2103D87A6C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCC98D37-7666-4880-9142-2103D87A6C35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCC98D37-7666-4880-9142-2103D87A6C35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCC98D37-7666-4880-9142-2103D87A6C35}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SecureDesignProject.sln.DotSettings.user b/SecureDesignProject.sln.DotSettings.user new file mode 100644 index 0000000..1b497e6 --- /dev/null +++ b/SecureDesignProject.sln.DotSettings.user @@ -0,0 +1,6 @@ + + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/SecureDesignProjectTests/ControllerTests/AuthControllerTests.cs b/SecureDesignProjectTests/ControllerTests/AuthControllerTests.cs new file mode 100644 index 0000000..2162eea --- /dev/null +++ b/SecureDesignProjectTests/ControllerTests/AuthControllerTests.cs @@ -0,0 +1,159 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using SecureDesignProject.Controllers; +using SecureDesignProject.Models; +using SecureDesignProject.Services; +using Xunit; + +public class AuthControllerTests +{ + private readonly Mock _authServiceMock; + private readonly Mock> _loggerMock; + private readonly AuthController _controller; + + public AuthControllerTests() + { + _authServiceMock = new Mock(); + _loggerMock = new Mock>(); + _controller = new AuthController(_loggerMock.Object, _authServiceMock.Object); + } + + [Fact] + public void Login_ShouldReturnViewResult() + { + // Act + var result = _controller.Login(); + + // Assert + var viewResult = Assert.IsType(result); + Assert.True((bool)_controller.TempData["hideLogout"]); + } + + [Fact] + public void Register_ShouldReturnViewResult() + { + // Act + var result = _controller.Register(); + + // Assert + var viewResult = Assert.IsType(result); + Assert.True((bool)_controller.TempData["hideLogout"]); + } + + [Fact] + public void Logout_ShouldRedirectToLogin_WhenSessionCookieIsNull() + { + // Arrange + var mockRequest = new Mock(); + mockRequest.Setup(r => r.Cookies["session_key"]).Returns((string)null); + var mockContext = new Mock(); + mockContext.Setup(c => c.Request).Returns(mockRequest.Object); + + _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object }; + + // Act + var result = _controller.Logout(); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Login", redirectResult.ActionName); + } + + [Fact] + public void Logout_ShouldInvalidateSessionAndRedirectToLogin_WhenSessionCookieExists() + { + // Arrange + var sessionKey = new byte[] { 0x01, 0x02, 0x03 }; + var mockRequest = new Mock(); + var mockResponse = new Mock(); + mockRequest.Setup(r => r.Cookies["session_key"]).Returns(Convert.ToHexString(sessionKey)); + var mockContext = new Mock(); + mockContext.Setup(c => c.Request).Returns(mockRequest.Object); + mockContext.Setup(c => c.Response).Returns(mockResponse.Object); + + _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object }; + + // Act + var result = _controller.Logout(); + + // Assert + _authServiceMock.Verify(s => s.InvalidateSession(sessionKey), Times.Once); + mockResponse.Verify(r => r.Cookies.Delete("session_key"), Times.Once); + var redirectResult = Assert.IsType(result); + Assert.Equal("Login", redirectResult.ActionName); + } + + [Fact] + public void Login_ShouldSetCookieAndRedirectToHome_WhenLoginSucceeds() + { + // Arrange + var loginDetails = new LoginDetails { Email = "test@example.com", Password = "password" }; + var sessionKey = new byte[] { 0x01, 0x02, 0x03 }; + _authServiceMock.Setup(s => s.AttemptLogin(loginDetails)) + .Returns((true, sessionKey)); + + var mockResponse = new Mock(); + var mockContext = new Mock(); + mockContext.Setup(c => c.Response).Returns(mockResponse.Object); + + _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object }; + + // Act + var result = _controller.Login(loginDetails); + + // Assert + _authServiceMock.Verify(s => s.AttemptLogin(loginDetails), Times.Once); + mockResponse.Verify( + r => r.Cookies.Append("session_key", Convert.ToHexString(sessionKey), It.IsAny()), + Times.Once); + var redirectResult = Assert.IsType(result); + Assert.Equal("Index", redirectResult.ActionName); + } + + [Fact] + public void Login_ShouldRedisplayViewWithError_WhenLoginFails() + { + // Arrange + var loginDetails = new LoginDetails { Email = "test@example.com", Password = "wrongpassword" }; + _authServiceMock.Setup(s => s.AttemptLogin(loginDetails)) + .Returns((false, null)); + + // Act + var result = _controller.Login(loginDetails); + + // Assert + var viewResult = Assert.IsType(result); + Assert.True((bool)_controller.TempData["hideLogout"]); + } + + [Fact] + public void Register_ShouldSetCookieAndRedirectToHome_WhenRegistrationSucceeds() + { + // Arrange + var registerDetails = new RegisterDetails { Email = "newuser@example.com", Password = "password" }; + var sessionKey = new byte[] { 0x01, 0x02, 0x03 }; + _authServiceMock.Setup(s => s.AttemptCreateAccount(registerDetails)) + .Returns((true, sessionKey)); + + var mockResponse = new Mock(); + var mockContext = new Mock(); + mockContext.Setup(c => c.Response).Returns(mockResponse.Object); + + _controller.ControllerContext = new ControllerContext { HttpContext = mockContext.Object }; + + // Act + var result = _controller.Register(registerDetails); + + // Assert + _authServiceMock.Verify(s => s.AttemptCreateAccount(registerDetails), Times.Once); + mockResponse.Verify( + r => r.Cookies.Append("session_key", Convert.ToHexString(sessionKey), It.IsAny()), + Times.Once); + var redirectResult = Assert.IsType(result); + Assert.Equal("Index", redirectResult.ActionName); + } + +} \ No newline at end of file diff --git a/SecureDesignProjectTests/ControllerTests/DashboardController.cs b/SecureDesignProjectTests/ControllerTests/DashboardController.cs new file mode 100644 index 0000000..cf43d9b --- /dev/null +++ b/SecureDesignProjectTests/ControllerTests/DashboardController.cs @@ -0,0 +1,64 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SecureDesignProject.Controllers; +using SecureDesignProject.Models; +using SecureDesignProject.Services; +using Xunit; + +public class DashboardControllerTests +{ + private readonly Mock _patientServiceMock; + private readonly Mock _caregiverServiceMock; + private readonly DashboardController _controller; + + public DashboardControllerTests() + { + _patientServiceMock = new Mock(); + _caregiverServiceMock = new Mock(); + _controller = new DashboardController(_patientServiceMock.Object, _caregiverServiceMock.Object); + } + + [Fact] + public void Patient_ShouldRedirectToError_WhenPatientNotFound() + { + // Arrange + _patientServiceMock.Setup(s => s.GetPatientInfoByAccountId(It.IsAny())).Returns((PatientInfo)null); + + // Act + var result = _controller.Patient(); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Error", redirectResult.ActionName); + } + + [Fact] + public void Patient_ShouldReturnViewWithPatientInfo_WhenPatientFound() + { + // Arrange + var patientInfo = new PatientInfo {PatientName = "John Doe" }; + _patientServiceMock.Setup(s => s.GetPatientInfoByAccountId(It.IsAny())).Returns(patientInfo); + + // Act + var result = _controller.Patient(); + + // Assert + var viewResult = Assert.IsType(result); + Assert.Equal(patientInfo, viewResult.Model); + } + + [Fact] + public void Caregiver_ShouldRedirectToError_WhenCaregiverOverviewNotFound() + { + // Arrange + _caregiverServiceMock.Setup(s => s.GetCaregiverOverview(It.IsAny())).Returns((CaregiverOverview)null); + + // Act + var result = _controller.Caregiver(); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Error", redirectResult.ActionName); + } +} diff --git a/SecureDesignProjectTests/ControllerTests/HomeControllerTests.cs b/SecureDesignProjectTests/ControllerTests/HomeControllerTests.cs new file mode 100644 index 0000000..6fbe0fb --- /dev/null +++ b/SecureDesignProjectTests/ControllerTests/HomeControllerTests.cs @@ -0,0 +1,112 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using SecureDesignProject.Controllers; +using SecureDesignProject.Models; +using SecureDesignProject.Services; + +public class HomeControllerTests +{ + private readonly Mock _dbServiceMock; + private readonly Mock> _loggerMock; + private readonly HomeController _controller; + + public HomeControllerTests() + { + _dbServiceMock = new Mock(); + _loggerMock = new Mock>(); + _controller = new HomeController(_loggerMock.Object, _dbServiceMock.Object); + } + + [Fact] + public void Index_ShouldRedirectToLogin_WhenSessionKeyIsNull() + { + // Arrange + var httpContext = new DefaultHttpContext(); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + + // Act + var result = _controller.Index(); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Login", redirectResult.ActionName); + Assert.Equal("Auth", redirectResult.ControllerName); + } + + [Fact] + public void Index_ShouldRedirectToPatientDashboard_WhenUserIsPatient() + { + // Arrange + var sessionKey = new byte[] { 1, 2, 3 }; + var account = new Account { UserType = UserType.Patient }; + var httpContext = new DefaultHttpContext(); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + + _dbServiceMock.Setup(db => db.GetAccountBySessionKey(sessionKey)).Returns(account); + + // Act + var result = _controller.Index(); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Patient", redirectResult.ActionName); + Assert.Equal("Dashboard", redirectResult.ControllerName); + } + + [Fact] + public void Index_ShouldRedirectToCaregiverDashboard_WhenUserIsCaregiver() + { + // Arrange + var sessionKey = new byte[] { 1, 2, 3 }; + var account = new Account { UserType = UserType.Caregiver }; + var httpContext = new DefaultHttpContext(); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + + _dbServiceMock.Setup(db => db.GetAccountBySessionKey(sessionKey)).Returns(account); + + // Act + var result = _controller.Index(); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Caregiver", redirectResult.ActionName); + Assert.Equal("Dashboard", redirectResult.ControllerName); + } + + [Fact] + public void Index_ShouldRedirectToLogin_WhenAccountIsInvalid() + { + // Arrange + var sessionKey = new byte[] { 1, 2, 3 }; + var httpContext = new DefaultHttpContext(); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + + _dbServiceMock.Setup(db => db.GetAccountBySessionKey(sessionKey)).Returns((Account)null); + + // Act + var result = _controller.Index(); + + // Assert + var redirectResult = Assert.IsType(result); + Assert.Equal("Login", redirectResult.ActionName); + Assert.Equal("Auth", redirectResult.ControllerName); + } + + [Fact] + public void Error_ShouldReturnErrorViewModel() + { + // Arrange + var httpContext = new DefaultHttpContext(); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + + // Act + var result = _controller.Error(); + + // Assert + var viewResult = Assert.IsType(result); + var model = Assert.IsType(viewResult.Model); + Assert.Equal(httpContext.TraceIdentifier, model.RequestId); + } +} diff --git a/SecureDesignProjectTests/GlobalUsings.cs b/SecureDesignProjectTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/SecureDesignProjectTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/SecureDesignProjectTests/HttpExtensionTests.cs b/SecureDesignProjectTests/HttpExtensionTests.cs new file mode 100644 index 0000000..20644da --- /dev/null +++ b/SecureDesignProjectTests/HttpExtensionTests.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.AspNetCore.Http; +using Moq; +using SecureDesignProject.Extensions; +using Xunit; + +public class HttpExtensionsTests +{ + [Fact] + public void SetSessionKeyCookie_ShouldSetCookieWithSessionKey() + { + // Arrange + var sessionKey = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + var mockResponse = new Mock(); + var mockCookies = new Mock(); + mockResponse.Setup(r => r.Cookies).Returns(mockCookies.Object); + + // Act + mockResponse.Object.SetSessionKeyCookie(sessionKey); + + // Assert + mockCookies.Verify(c => c.Append( + "session_key", + Convert.ToHexString(sessionKey), + It.Is(o => o.Expires > DateTime.Now && o.HttpOnly)), Times.Once); + } + + [Fact] + public void GetSessionCookie_ShouldReturnSessionKey_WhenCookieExists() + { + // Arrange + var sessionKeyHex = "01020304"; + var expectedSessionKey = Convert.FromHexString(sessionKeyHex); + var mockRequest = new Mock(); + var mockCookies = new Mock(); + + mockCookies.Setup(c => c["session_key"]).Returns(sessionKeyHex); + mockRequest.Setup(r => r.Cookies).Returns(mockCookies.Object); + + // Act + var result = mockRequest.Object.GetSessionCookie(); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedSessionKey, result); + } + + [Fact] + public void GetSessionCookie_ShouldReturnNull_WhenCookieDoesNotExist() + { + // Arrange + var mockRequest = new Mock(); + var mockCookies = new Mock(); + + mockCookies.Setup(c => c["session_key"]).Returns((string)null); + mockRequest.Setup(r => r.Cookies).Returns(mockCookies.Object); + + // Act + var result = mockRequest.Object.GetSessionCookie(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetAccountId_ShouldReturnAccountId_WhenContextHasAccountId() + { + // Arrange + var accountId = Guid.NewGuid(); + var context = new DefaultHttpContext(); + context.Items["accountId"] = accountId; + + // Act + var result = context.GetAccountId(); + + // Assert + Assert.Equal(accountId, result); + } + + [Fact] + public void GetAccountId_ShouldReturnEmptyGuid_WhenContextDoesNotHaveAccountId() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + var result = context.GetAccountId(); + + // Assert + Assert.Equal(Guid.Empty, result); + } +} diff --git a/SecureDesignProjectTests/SecureDesignProjectTests.csproj b/SecureDesignProjectTests/SecureDesignProjectTests.csproj new file mode 100644 index 0000000..ec4800f --- /dev/null +++ b/SecureDesignProjectTests/SecureDesignProjectTests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/SecureDesignProjectTests/ServiceTests/AuthServiceTests.cs b/SecureDesignProjectTests/ServiceTests/AuthServiceTests.cs new file mode 100644 index 0000000..682f572 --- /dev/null +++ b/SecureDesignProjectTests/ServiceTests/AuthServiceTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using Moq; +using SecureDesignProject.Models; +using SecureDesignProject.Services; +using Xunit; + +namespace SecureDesignProject.Tests +{ + public class AuthServiceTests + { + private readonly Mock _mockDbService; + private readonly AuthService _authService; + + public AuthServiceTests() + { + _mockDbService = new Mock(); + _authService = new AuthService(_mockDbService.Object); + } + + + [Fact] + public void AttemptCreateAccount_WithValidDetails_ReturnsSuccess() + { + // Arrange + var email = "newuser@example.com"; + var password = "Password123"; + var registerDetails = new RegisterDetails + { + Email = email, + Password = password, + FirstName = "John", + LastName = "Doe" + }; + + _mockDbService.Setup(db => db.GetAccountByEmail(email)).Returns((Account)null); + _mockDbService.Setup(db => db.InsertAccount(It.IsAny())).Returns(new Account { AccountId = Guid.NewGuid() }); + _mockDbService.Setup(db => db.InsertPatient(It.IsAny())).Returns(new Patient { PatientId = Guid.NewGuid() }); + + // Act + var result = _authService.AttemptCreateAccount(registerDetails); + + // Assert + Assert.True(result.success); + Assert.NotEmpty(result.SessionKey); + } + + [Fact] + public void ValidateSession_WithValidKey_ReturnsValid() + { + // Arrange + var sessionKey = RandomNumberGenerator.GetBytes(255); + var session = new Session + { + SessionId = Guid.NewGuid(), + AccountId = Guid.NewGuid(), + SessionKey = sessionKey, + IsValid = true + }; + + _mockDbService.Setup(db => db.GetSessionByKey(sessionKey)).Returns(session); + + // Act + var result = _authService.ValidateSession(sessionKey); + + // Assert + Assert.True(result.isValid); + Assert.Equal(session.AccountId, result.accountId); + } + + [Fact] + public void InvalidateSession_WithValidKey_MarksSessionInvalid() + { + // Arrange + var sessionKey = RandomNumberGenerator.GetBytes(255); + var session = new Session + { + SessionId = Guid.NewGuid(), + AccountId = Guid.NewGuid(), + SessionKey = sessionKey, + IsValid = true, + Created = DateTime.Now, + LastSeen = DateTime.Now, + Expires = DateTime.Now.AddMinutes(10) + }; + + _mockDbService.Setup(db => db.GetSessionByKey(sessionKey)).Returns(session); + _mockDbService.Setup(db => db.UpdateSession(It.IsAny())).Verifiable(); + + // Act + _authService.InvalidateSession(sessionKey); + + // Assert + _mockDbService.Verify(db => db.UpdateSession(It.Is(s => !s.IsValid))); + } + } +} diff --git a/SecureDesignProjectTests/ServiceTests/CaregiverServiceTests.cs b/SecureDesignProjectTests/ServiceTests/CaregiverServiceTests.cs new file mode 100644 index 0000000..d4a5bba --- /dev/null +++ b/SecureDesignProjectTests/ServiceTests/CaregiverServiceTests.cs @@ -0,0 +1,88 @@ +using Moq; +using SecureDesignProject.Models; +using SecureDesignProject.Services; + +public class CaregiverServiceTests +{ + private readonly Mock _mockDbService; + private readonly CaregiverService _caregiverService; + + public CaregiverServiceTests() + { + _mockDbService = new Mock(); + _caregiverService = new CaregiverService(_mockDbService.Object); + } + + [Fact] + public void GetCaregiverOverview_CaregiverNotFound_ReturnsNull() + { + // Arrange + var accountId = Guid.NewGuid(); + _mockDbService.Setup(db => db.GetCaregiverByAccountId(accountId)).Returns((Caregiver)null); + + // Act + var result = _caregiverService.GetCaregiverOverview(accountId); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetCaregiverOverview_NoAssignedPatients_ReturnsOverviewWithEmptyPatients() + { + // Arrange + var accountId = Guid.NewGuid(); + var caregiverId = Guid.NewGuid(); + var caregiver = new Caregiver + { + CaregiverId = caregiverId, + FirstName = "John", + LastName = "Doe" + }; + + _mockDbService.Setup(db => db.GetCaregiverByAccountId(accountId)).Returns(caregiver); + _mockDbService.Setup(db => db.GetPatientsByAssignedCaregiver(caregiverId)).Returns(new List()); + + // Act + var result = _caregiverService.GetCaregiverOverview(accountId); + + // Assert + Assert.NotNull(result); + Assert.Equal(caregiverId, result.CaregiverId); + Assert.Equal("John Doe", result.CaregiverName); + Assert.Empty(result.Patients); + } + + [Fact] + public void GetCaregiverOverview_WithAssignedPatients_ReturnsOverviewWithPatients() + { + // Arrange + var accountId = Guid.NewGuid(); + var caregiverId = Guid.NewGuid(); + var caregiver = new Caregiver + { + CaregiverId = caregiverId, + FirstName = "Alice", + LastName = "Smith" + }; + var patients = new List + { + new Patient { PatientId = Guid.NewGuid(), FirstName = "Patient1", LastName = "Test1" }, + new Patient { PatientId = Guid.NewGuid(), FirstName = "Patient2", LastName = "Test2" } + }; + + _mockDbService.Setup(db => db.GetCaregiverByAccountId(accountId)).Returns(caregiver); + _mockDbService.Setup(db => db.GetPatientsByAssignedCaregiver(caregiverId)).Returns(patients); + + // Act + var result = _caregiverService.GetCaregiverOverview(accountId); + + // Assert + Assert.NotNull(result); + Assert.Equal(caregiverId, result.CaregiverId); + Assert.Equal("Alice Smith", result.CaregiverName); + Assert.Equal(2, result.Patients.Length); + Assert.Contains(result.Patients, p => p.Item1 == "Patient1 Test1" && p.Item2 == patients[0].PatientId); + Assert.Contains(result.Patients, p => p.Item1 == "Patient2 Test2" && p.Item2 == patients[1].PatientId); + } +} diff --git a/SecureDesignProjectTests/ServiceTests/PatientServiceTests.cs b/SecureDesignProjectTests/ServiceTests/PatientServiceTests.cs new file mode 100644 index 0000000..64c7d7c --- /dev/null +++ b/SecureDesignProjectTests/ServiceTests/PatientServiceTests.cs @@ -0,0 +1,155 @@ +using Moq; +using SecureDesignProject.Models; +using SecureDesignProject.Services; + +public class PatientServiceTests +{ + private readonly Mock _mockDbService; + private readonly PatientService _patientService; + + public PatientServiceTests() + { + _mockDbService = new Mock(); + _patientService = new PatientService(_mockDbService.Object); + } + + + + [Fact] + public void GetAppointmentInfo_ShouldReturnNewAppointment_WhenAppointmentDoesNotExist() + { + // Arrange + Guid appointmentId = Guid.NewGuid(); + _mockDbService.Setup(db => db.GetAppointmentById(appointmentId)).Returns((Appointment)null); + + // Act + var result = _patientService.GetAppointmentInfo(appointmentId); + + // Assert + Assert.NotNull(result); + Assert.Equal(appointmentId, result.AppointmentId); + Assert.Empty(result.Patient); + Assert.Empty(result.Caregiver); + Assert.Empty(result.AppointmentTime); + Assert.Empty(result.AppointmentDuration); + Assert.Empty(result.Notes); + } + + + + [Fact] + public void AttemptUpdateAppointment_ShouldReturnFalse_WhenParsingFails() + { + // Arrange + var appointmentInfo = new AppointmentInfo + { + AppointmentTime = "Invalid Time", + AppointmentDuration = "Invalid Duration" + }; + + // Act + var result = _patientService.AttemptUpdateAppointment(appointmentInfo); + + // Assert + Assert.False(result); + } + + [Fact] + public void AttemptUpdateAppointment_ShouldInsertNewAppointment_WhenAppointmentDoesNotExist() + { + // Arrange + var appointmentInfo = new AppointmentInfo + { + AppointmentId = Guid.NewGuid(), + AppointmentTime = DateTime.Now.ToString(), + AppointmentDuration = "60", + CreatorId = Guid.NewGuid(), + PatientId = Guid.NewGuid(), + Notes = "New Appointment" + }; + + _mockDbService.Setup(db => db.GetAppointmentById(appointmentInfo.AppointmentId ?? Guid.NewGuid())).Returns((Appointment)null); + + // Act + var result = _patientService.AttemptUpdateAppointment(appointmentInfo); + + // Assert + Assert.True(result); + _mockDbService.Verify(db => db.InsertAppointment(It.IsAny()), Times.Once); + } + + [Fact] + public void AttemptUpdateAppointment_ShouldUpdateExistingAppointment_WhenAppointmentExists() + { + // Arrange + var appointmentInfo = new AppointmentInfo + { + AppointmentId = Guid.NewGuid(), + AppointmentTime = DateTime.Now.ToString(), + AppointmentDuration = "60", + Notes = "Updated Appointment" + }; + + var existingAppointment = new Appointment + { + AppointmentId = appointmentInfo.AppointmentId ?? Guid.NewGuid(), + }; + + _mockDbService.Setup(db => db.GetAppointmentById(appointmentInfo.AppointmentId ?? Guid.NewGuid())).Returns(existingAppointment); + + // Act + var result = _patientService.AttemptUpdateAppointment(appointmentInfo); + + // Assert + Assert.True(result); + _mockDbService.Verify(db => db.UpdateAppointment(It.IsAny()), Times.Once); + } + + [Fact] + public void GetPatientInfoByAccountId_ShouldReturnNull_WhenPatientDoesNotExist() + { + // Arrange + Guid accountId = Guid.NewGuid(); + _mockDbService.Setup(db => db.GetPatientByAccountId(accountId)).Returns((Patient)null); + + // Act + var result = _patientService.GetPatientInfoByAccountId(accountId); + + // Assert + Assert.Null(result); + } + + [Fact] + public void AttemptUpdateAddress_ShouldReturnFalse_WhenPatientDoesNotExist() + { + // Arrange + Guid accountId = Guid.NewGuid(); + var address = new Address(); + + _mockDbService.Setup(db => db.GetPatientByAccountId(accountId)).Returns((Patient)null); + + // Act + var result = _patientService.AttemptUpdateAddress(accountId, address); + + // Assert + Assert.False(result); + } + + [Fact] + public void AttemptUpdateAddress_ShouldUpdateAddress_WhenPatientExists() + { + // Arrange + Guid accountId = Guid.NewGuid(); + var address = new Address { Line1 = "Line 1", Line2 = "Line 2", City = "City", Postcode = "Postcode" }; + var patient = new Patient { PatientId = Guid.NewGuid(), Address = "Old Address" }; + + _mockDbService.Setup(db => db.GetPatientByAccountId(accountId)).Returns(patient); + + // Act + var result = _patientService.AttemptUpdateAddress(accountId, address); + + // Assert + Assert.True(result); + _mockDbService.Verify(db => db.UpdatePatient(It.IsAny()), Times.Once); + } +} diff --git a/Services/AuthService.cs b/Services/AuthService.cs index 66de3cb..61f9e85 100644 --- a/Services/AuthService.cs +++ b/Services/AuthService.cs @@ -5,8 +5,18 @@ namespace SecureDesignProject.Services; +/// +/// The AuthService class provides methods for handling user authentication, +/// account creation, session management, and password hashing. +/// public class AuthService(DatabaseService dbService) { + /// + /// Attempts to log in a user by verifying their credentials. + /// If successful, generates and returns a session key. + /// + /// User login credentials, including email and password. + /// A tuple containing a success flag and the session key. public (bool success, byte[] sessionKey) AttemptLogin(LoginDetails loginDetails) { var account = dbService.GetAccountByEmail(loginDetails.Email); @@ -32,6 +42,11 @@ public class AuthService(DatabaseService dbService) return (true, newSession.SessionKey); } + /// + /// Validates a session using its key and returns the associated account ID if valid. + /// + /// The session key to validate. + /// A tuple containing a validity flag and the associated account ID. public (bool isValid, Guid accountId) ValidateSession(byte[] key) { var session = dbService.GetSessionByKey(key); @@ -39,6 +54,11 @@ public class AuthService(DatabaseService dbService) return (session != null, session?.AccountId ?? Guid.Empty); } + /// + /// Attempts to create a new user account. If successful, logs in the user automatically. + /// + /// Details for the new account, including email, password, and name. + /// A tuple containing a success flag and the new session key. public (bool success, byte[] SessionKey) AttemptCreateAccount(RegisterDetails registerDetails) { var existingAccount = dbService.GetAccountByEmail(registerDetails.Email); @@ -80,6 +100,10 @@ public class AuthService(DatabaseService dbService) return (success, sessionKey); } + /// + /// Invalidates an existing session by marking it as expired. + /// + /// The session key to invalidate. public void InvalidateSession(byte[] key) { var session = dbService.GetSessionByKey(key); @@ -94,6 +118,12 @@ public void InvalidateSession(byte[] key) }); } + /// + /// Hashes a password using the Argon2id algorithm and a provided salt. + /// + /// The plaintext password to hash. + /// The salt to use for hashing. + /// The hashed password as a byte array. private static byte[] HashPassword(string password, byte[] salt) { var argon2Id = new Argon2id(Encoding.UTF8.GetBytes(password)); @@ -107,11 +137,22 @@ private static byte[] HashPassword(string password, byte[] salt) return hash; } + /// + /// Generates a cryptographically secure random salt. + /// + /// A randomly generated salt as a byte array. private static byte[] GenerateSalt() { return RandomNumberGenerator.GetBytes(128); } + /// + /// Verifies if a plaintext password matches the stored hash using the provided salt. + /// + /// The plaintext password to verify. + /// The stored hashed password. + /// The salt used during hashing. + /// True if the password matches, false otherwise. private static bool VerifyHash(string password, byte[] hash, byte[] salt) { var newHash = HashPassword(password, salt); diff --git a/Services/CaregiverService.cs b/Services/CaregiverService.cs index b05a809..7cd3df4 100644 --- a/Services/CaregiverService.cs +++ b/Services/CaregiverService.cs @@ -2,8 +2,22 @@ namespace SecureDesignProject.Services; + +/// +/// The CaregiverService class provides functionality related to caregivers, +/// including retrieving an overview of their assigned patients. +/// public class CaregiverService(DatabaseService dbService) { + + /// + /// Retrieves an overview of a caregiver, including their name and a list of assigned patients. + /// + /// The account ID associated with the caregiver. + /// + /// A object containing caregiver details and their assigned patients, + /// or null if no caregiver is associated with the provided account ID. + /// public CaregiverOverview? GetCaregiverOverview(Guid accountId) { var caregiver = dbService.GetCaregiverByAccountId(accountId); diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index c7087fd..e79e068 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -1,44 +1,43 @@ using System.Data.SqlClient; using Dapper; using SecureDesignProject.Models; +using Record = SecureDesignProject.Models.Record; namespace SecureDesignProject.Services; -public class DatabaseService +public class DatabaseService(string connectionString) { - private string ConnectionString = "Server=localhost,1433;Database=HealthServiceDb;User Id=SA;Password=Super-Secret-Password;TrustServerCertificate=true;Encrypt=false;"; - public Account? GetAccountByEmail(string email) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault("SELECT * FROM Accounts WHERE Email = @email", new { email }); } public Appointment? GetAppointmentById(Guid appointmentId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault("SELECT * FROM Appointments WHERE AppointmentId = @appointmentId", new { appointmentId }); } public Record? GetPatientRecordById(Guid recordId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault("SELECT * FROM PatientsRecords WHERE RecordId = @recordId", new { recordId }); } public Session? GetSessionByKey(byte[] key) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault("SELECT * FROM Sessions WHERE SessionKey = @key AND IsValid=1", new { key }); } public Session? UpdateSession(Session session) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute(""" UPDATE Sessions @@ -52,7 +51,7 @@ UPDATE Sessions public Patient? UpdatePatient(Patient patient) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute(""" UPDATE Patients @@ -66,7 +65,7 @@ UPDATE Patients public Appointment UpdateAppointment(Appointment appointment) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute(""" UPDATE Appointments @@ -80,7 +79,7 @@ UPDATE Appointments public Record UpdateRecord(Record record) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute(""" UPDATE PatientsRecords @@ -94,7 +93,7 @@ UPDATE PatientsRecords public Account InsertAccount(Account account) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute("insert into Accounts(AccountId, Email, HashedPassword, UserType, Salt) values (@AccountId, @Email, @HashedPassword, @UserType, @Salt)", account); @@ -103,7 +102,7 @@ public Account InsertAccount(Account account) public Appointment InsertAppointment(Appointment appointment) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute("insert into Appointments(AppointmentId, PatientId, CreatorId, AppointmentTime, Duration, Notes) values (@AppointmentId, @PatientId, @CreatorId, @AppointmentTime, @Duration, @Notes)", appointment); @@ -112,7 +111,7 @@ public Appointment InsertAppointment(Appointment appointment) public Record InsertRecord(Record record) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute("insert into PatientRecords(RecordId, PatientId, CreatorId, RecordName, RecordData) values (@recordId, @patientId, @creatorId, @recordName, @recordData)", record); @@ -121,7 +120,7 @@ public Record InsertRecord(Record record) public Patient InsertPatient(Patient patient) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute(""" insert into Patients(PatientId, AccountId, FirstName, LastName, Address) @@ -134,7 +133,7 @@ insert into Patients(PatientId, AccountId, FirstName, LastName, Address) public Session InsertSession(Session session) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); db.Execute(""" insert into Sessions(SessionId, AccountId, SessionKey, IsValid, Created, LastSeen, Expires) @@ -147,7 +146,7 @@ insert into Sessions(SessionId, AccountId, SessionKey, IsValid, Created, LastSee public Account? GetAccountBySessionKey(byte[] sessionKey) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault(""" SELECT * @@ -163,7 +162,7 @@ FROM Sessions public Patient? GetPatientByAccountId(Guid accountId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault(""" SELECT * @@ -175,7 +174,7 @@ FROM Patients public Patient? GetPatientByPatientId(Guid patientId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault(""" SELECT * @@ -186,7 +185,7 @@ FROM Patients } public Caregiver? GetCaregiverByAccountId(Guid accountId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault(""" SELECT * @@ -198,7 +197,7 @@ FROM Caregivers public Caregiver? GetCaregiverById(Guid caregiverId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.QuerySingleOrDefault(""" SELECT * @@ -210,7 +209,7 @@ FROM Caregivers public IEnumerable GetCaregiversByAssignedPatient(Guid patientId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.Query(""" SELECT * @@ -227,7 +226,7 @@ FROM Caregivers public IEnumerable GetPatientsByAssignedCaregiver(Guid caregiverId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.Query(""" SELECT * @@ -244,7 +243,7 @@ FROM Patients public IEnumerable GetAppointmentsByPatient(Guid patientId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.Query(""" SELECT * @@ -256,7 +255,7 @@ FROM Appointments public IEnumerable GetRecordsByPatient(Guid patientId) { - using var db = new SqlConnection(ConnectionString); + using var db = new SqlConnection(connectionString); return db.Query(""" SELECT * diff --git a/Services/PatientService.cs b/Services/PatientService.cs index 256d917..cd1ee6f 100644 --- a/Services/PatientService.cs +++ b/Services/PatientService.cs @@ -3,8 +3,20 @@ namespace SecureDesignProject.Services; +/// +/// Provides services related to patient management, including fetching patient records, +/// appointments, and updating patient information. +/// public class PatientService(DatabaseService dbService) { + /// + /// Retrieves a patient record by its ID or returns a new empty record if not found. + /// + /// The ID of the patient record, or null to indicate no specific ID. + /// + /// A object representing the patient's record. + /// If the record does not exist, a new empty record is returned. + /// public PatientRecord GetPatientRecord(Guid? recordId) { var existingRecord = dbService.GetPatientRecordById(recordId ?? Guid.Empty); @@ -34,6 +46,15 @@ public PatientRecord GetPatientRecord(Guid? recordId) }; } + + /// + /// Retrieves information about an appointment by its ID or returns a new empty appointment if not found. + /// + /// The ID of the appointment, or null to indicate no specific ID. + /// + /// An object containing the appointment details. + /// If the appointment does not exist, a new empty appointment is returned. + /// public AppointmentInfo GetAppointmentInfo(Guid? appointmentId) { var appointment = dbService.GetAppointmentById(appointmentId ?? Guid.Empty); @@ -65,6 +86,13 @@ public AppointmentInfo GetAppointmentInfo(Guid? appointmentId) }; } + /// + /// Attempts to update an existing patient record with new data. + /// + /// The updated patient record. + /// + /// True if the record was successfully updated; otherwise, false if the record does not exist. + /// public bool AttemptUpdatePatientRecord(PatientRecord patientRecord) { @@ -82,6 +110,13 @@ public bool AttemptUpdatePatientRecord(PatientRecord patientRecord) return true; } + /// + /// Attempts to update or create an appointment based on the provided information. + /// + /// The updated or new appointment information. + /// + /// True if the operation succeeds; otherwise, false if parsing of input data fails. + /// public bool AttemptUpdateAppointment(AppointmentInfo appointmentInfo) { // parse string data @@ -126,18 +161,36 @@ public bool AttemptUpdateAppointment(AppointmentInfo appointmentInfo) return true; } + /// + /// Retrieves detailed patient information by their account ID. + /// + /// The account ID of the patient. + /// + /// A object if the patient exists; otherwise, null. + /// public PatientInfo? GetPatientInfoByAccountId(Guid accountId) { var patient = dbService.GetPatientByAccountId(accountId); return patient == null ? null : GetPatientInfo(patient); } + + /// + /// Retrieves detailed patient information by their patient ID. + /// + /// The ID of the patient. + /// + /// A object if the patient exists; otherwise, null. + /// public PatientInfo? GetPatientInfoByPatientId(Guid patientId) { var patient = dbService.GetPatientByPatientId(patientId); return patient == null ? null : GetPatientInfo(patient); } + /// + /// Retrieves detailed patient information for the given patient object. + /// private PatientInfo GetPatientInfo(Patient patient) { var caregivers = dbService @@ -205,6 +258,9 @@ private PatientInfo GetPatientInfo(Patient patient) }; } + /// + /// Retrieves the address of a patient by their account ID. + /// public Address? GetAddressByAccountId(Guid accountId) { var patient = dbService.GetPatientByAccountId(accountId); @@ -224,6 +280,9 @@ private PatientInfo GetPatientInfo(Patient patient) }; } + /// + /// Attempts to update the address of a patient by their account ID. + /// public bool AttemptUpdateAddress(Guid accountId, Address address) { var patient = dbService.GetPatientByAccountId(accountId); diff --git a/Views/Auth/Register.cshtml b/Views/Auth/Register.cshtml index b35a623..87759fe 100644 --- a/Views/Auth/Register.cshtml +++ b/Views/Auth/Register.cshtml @@ -62,17 +62,6 @@ @class = "m-3 text-danger" }) - @Html.TextBoxFor(m => m.InviteCode, new - { - id = "code-box", - type="number", - placeholder="Code", - @class = "form-control m-3 w-auto" - }) - @Html.ValidationMessageFor(model => model.InviteCode, default, new - { - @class = "m-3 text-danger" - }) diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 76698af..36ed910 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -22,9 +22,6 @@ -