diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Controllers/api/UserController.cs b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/api/UserController.cs new file mode 100644 index 000000000..d2b97e13c --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/api/UserController.cs @@ -0,0 +1,60 @@ +namespace LearningHub.Nhs.AdminUI.Controllers.Api +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using elfhHub.Nhs.Models.Common; + using elfhHub.Nhs.Models.Enums; + using LearningHub.Nhs.AdminUI.Interfaces; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + /// + /// The UserController class. + /// + [Authorize] + [Route("api/[controller]")] + [ApiController] + public class UserController : BaseApiController + { + /// + /// The elfh user service.. + /// + private IUserService userService; + + /// + /// Initializes a new instance of the class. + /// + /// The userService. + /// loginWizardService. + /// logger. + /// Settings. + public UserController(IUserService userService, ILogger logger) + : base(logger) + { + this.userService = userService; + } + + /// + /// The SessionTimeout. + /// + /// The . + [HttpPost("browser-close")] + public IActionResult BrowserClose() + { + // Add browser close to the UserHistory + UserHistoryViewModel userHistory = new UserHistoryViewModel() + { + UserId = this.CurrentUserId, + UserHistoryTypeId = (int)UserHistoryType.Logout, + Detail = @"User browser closed", + }; + + this.userService.StoreUserHistory(userHistory); + + return this.Ok(true); + } + } +} diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_Layout.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_Layout.cshtml index d12be205a..aa098dc6a 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_Layout.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_Layout.cshtml @@ -78,7 +78,14 @@ @**@ + @RenderSection("Scripts", required: false) + + diff --git a/AdminUI/LearningHub.Nhs.AdminUI/wwwroot/js/PageUnload.js b/AdminUI/LearningHub.Nhs.AdminUI/wwwroot/js/PageUnload.js new file mode 100644 index 000000000..8c458bee1 --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/wwwroot/js/PageUnload.js @@ -0,0 +1,13 @@ + // This function will be called when the browser window is closed or unloaded + function tellServerBrowserClosed() { + // Send an asynchronous request to the server when the browser is closed + fetch('/api/user/browser-close', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ message: 'Browser closed' }) + }) + .then(response => response.json()) + .catch(error => console.error('Error sending data to server:', error)); + } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index ccb06c921..f0f2da3c1 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -196,6 +196,11 @@ public Settings() /// public int PasswordRequestLimit { get; set; } + /// + /// Gets or sets the ConcurrentId. + /// + public int ConcurrentId { get; set; } + /// /// Gets or sets the SupportUrls. /// diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/UserController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/UserController.cs index fc5c5579a..eb3cc901b 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/UserController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/UserController.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; + using elfhHub.Nhs.Models.Enums; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.Authorization; @@ -62,6 +63,26 @@ public async Task CurrentProfile() return this.Ok(await this.userService.GetCurrentUserProfileAsync()); } + /// + /// The SessionTimeout. + /// + /// The . + [HttpPost("browser-close")] + public IActionResult BrowserClose() + { + // Add browser close to the UserHistory + UserHistoryViewModel userHistory = new UserHistoryViewModel() + { + UserId = this.CurrentUserId, + UserHistoryTypeId = (int)UserHistoryType.Logout, + Detail = @"User browser closed", + }; + + this.userService.StoreUserHistory(userHistory); + + return this.Ok(true); + } + /// /// Get current user's basic details. /// diff --git a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs index 92d550393..ece318e32 100644 --- a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs @@ -6,6 +6,8 @@ namespace LearningHub.Nhs.WebUI.Controllers using System.Linq; using System.Net.Http; using System.Threading.Tasks; + using elfhHub.Nhs.Models.Common; + using elfhHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Content; using LearningHub.Nhs.Models.Enums.Content; using LearningHub.Nhs.Models.Extensions; @@ -20,6 +22,7 @@ namespace LearningHub.Nhs.WebUI.Controllers using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.FeatureManagement; @@ -52,6 +55,7 @@ public class HomeController : BaseController /// Dashboard service. /// Content service. /// featureManager. + /// config. public HomeController( IHttpClientFactory httpClientFactory, IWebHostEnvironment hostingEnvironment, @@ -62,7 +66,8 @@ public HomeController( LearningHubAuthServiceConfig authConfig, IDashboardService dashboardService, IContentService contentService, - IFeatureManager featureManager) + IFeatureManager featureManager, + Microsoft.Extensions.Configuration.IConfiguration configuration) : base(hostingEnvironment, httpClientFactory, logger, settings.Value) { this.authConfig = authConfig; @@ -71,6 +76,7 @@ public HomeController( this.dashboardService = dashboardService; this.contentService = contentService; this.featureManager = featureManager; + this.configuration = configuration; } /// @@ -200,6 +206,7 @@ public async Task Index(string myLearningDashboard = "my-in-progr { if (this.User?.Identity.IsAuthenticated == true) { + this.Settings.ConcurrentId = this.CurrentUserId; this.Logger.LogInformation("User is authenticated: User is {fullname} and userId is: {lhuserid}", this.User.Identity.GetCurrentName(), this.User.Identity.GetCurrentUserId()); if (this.User.IsInRole("Administrator") || this.User.IsInRole("BlueUser") || this.User.IsInRole("ReadOnly") || this.User.IsInRole("BasicUser")) { @@ -369,11 +376,41 @@ public IActionResult SessionTimeout(string returnUrl = "/") return this.Redirect(returnUrl); } + // Add successful logout to the UserHistory + UserHistoryViewModel userHistory = new UserHistoryViewModel() + { + UserId = this.Settings.ConcurrentId, + UserHistoryTypeId = (int)UserHistoryType.Logout, + Detail = @"User session time out", + }; + + this.userService.StoreUserHistory(userHistory); + this.ViewBag.AuthTimeout = this.authConfig.AuthTimeout; this.ViewBag.ReturnUrl = returnUrl; + return this.View(); } + /// + /// The SessionTimeout. + /// + /// The . + [HttpPost("browser-close")] + public IActionResult BrowserClose() + { + // Add browser close to the UserHistory + UserHistoryViewModel userHistory = new UserHistoryViewModel() + { + UserId = this.CurrentUserId, + UserHistoryTypeId = (int)UserHistoryType.Logout, + Detail = @"User browser closed", + }; + + this.userService.StoreUserHistory(userHistory); + return this.Ok(true); + } + /// /// The SitemapXml. /// diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml index 0c96940f7..b999a6fb2 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml @@ -133,19 +133,19 @@ @if (!ViewData["DisableValidation"]?.Equals(true) ?? true) { + asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js" + asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive" + asp-suppress-fallback-integrity="true" + crossorigin="anonymous" + integrity="sha512-xq+Vm8jC94ynOikewaQXMEkJIOBp7iArs3IhFWSWdRT3Pq8wFz46p+ZDFAR7kHnSFf+zUv52B3prRYnbDRdgog=="> + } @@ -161,6 +161,13 @@ @RenderSection("Scripts", required: false) + + + diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index e67ceaa8b..112ba119e 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -39,6 +39,7 @@ "Restricted": false, "PasswordRequestLimitingPeriod": 1, // minutes "PasswordRequestLimit": 2, + "ConcurrentId": 0, "AzureBlobSettings": { "ConnectionString": "", "UploadContainer": "" diff --git a/LearningHub.Nhs.WebUI/wwwroot/js/PageUnload.js b/LearningHub.Nhs.WebUI/wwwroot/js/PageUnload.js new file mode 100644 index 000000000..8c458bee1 --- /dev/null +++ b/LearningHub.Nhs.WebUI/wwwroot/js/PageUnload.js @@ -0,0 +1,13 @@ + // This function will be called when the browser window is closed or unloaded + function tellServerBrowserClosed() { + // Send an asynchronous request to the server when the browser is closed + fetch('/api/user/browser-close', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ message: 'Browser closed' }) + }) + .then(response => response.json()) + .catch(error => console.error('Error sending data to server:', error)); + } \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index 06e1a1598..aba4a7f8f 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -537,6 +537,7 @@ + diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/ELFH Database Scripts/ConCurrentUserSessionsTablesandSP.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/ELFH Database Scripts/ConCurrentUserSessionsTablesandSP.sql new file mode 100644 index 000000000..0e8929a3a --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/ELFH Database Scripts/ConCurrentUserSessionsTablesandSP.sql @@ -0,0 +1,193 @@ +ALTER TABLE [dbo].[userHistoryTBL] +ADD sessionId NVARCHAR(50) NULL, + isActive BIT NULL; + +GO + +-------------------------------------------------------------------------- +-- Chris Bain 08 Aug 2014 - Initial Build +-- Killian Davies 12 Aug 2015 - Added userAgent info (for Login history event) +-- Chris Bain 18 Nov 2016 - Added tenant Id +-- Jignesh Jethwani 02 Mar 2018 - Save details to User History Attribute, add url referer +-- Swapnamol Abraham 01 Apr 2025 - Save session and isactive details +-------------------------------------------------------------------------- +ALTER PROCEDURE [dbo].[proc_UserHistoryInsert] + @userId int = 0, + @historyTypeId int, + @detailedInfo NVARCHAR(1000) = NULL, + @userAgent NVARCHAR(1000) = NULL, + @browserName NVARCHAR(1000) = NULL, + @browserVersion NVARCHAR(1000) = NULL, + @urlReferer NVARCHAR(1000) = NULL, + @loginIP NVARCHAR(50) = NULL, + @loginSuccessFul bit = NULL, + @tenantId INT, + @sessionId NVARCHAR(50) = NULL, + @isActive bit = NULL, + @amendUserId INT, + @amendDate DATETIMEOFFSET = NULL +AS +BEGIN + + DECLARE @UserHistoryId int + DECLARE @detailInfoAttributeId int, @userAgentAttributeId int, @browserNameAttributeId int, @browserVersionAttributeId int, @urlRefererAttributeId int, @loginSuccessFulAttributeId int + SET @amendDate = CoalEsce(@amendDate,SysDateTimeOffset()) + + INSERT INTO userHistoryTBL (userId, userHistoryTypeId, tenantId,sessionId,isActive, createdDate) + VALUES (@userId, @historyTypeId, @tenantId, @sessionId,@isActive, @amendDate) + + If(@historyTypeId = 10) + BEGIN + UPDATE userHistoryTBL SET isActive = 0 Where UserId = @userId AND isActive =1 + + END + + SET @UserHistoryId = CAST(SCOPE_IDENTITY() AS int) + + SELECT @detailInfoAttributeId = attributeId FROM [dbo].[attributeTBL] WHERE [attributeName] = 'UserHistory_DetailedInfo' AND deleted = 0 + SELECT @userAgentAttributeId = attributeId FROM [dbo].[attributeTBL] WHERE [attributeName] = 'UserHistory_UserAgent' AND deleted = 0 + SELECT @browserNameAttributeId = attributeId FROM [dbo].[attributeTBL] WHERE [attributeName] = 'UserHistory_BrowserName' AND deleted = 0 + SELECT @browserVersionAttributeId = attributeId FROM [dbo].[attributeTBL] WHERE [attributeName] = 'UserHistory_BrowserVersion' AND deleted = 0 + SELECT @urlRefererAttributeId = attributeId FROM [dbo].[attributeTBL] WHERE [attributeName] = 'UserHistory_UrlReferer' AND deleted = 0 + SELECT @loginSuccessFulAttributeId = attributeId FROM [dbo].[attributeTBL] WHERE [attributeName] = 'UserHistory_LoginSuccessful' AND deleted = 0 + + -- DetailedInfo + IF @detailInfoAttributeId > 0 AND @detailedInfo IS NOT NULL + BEGIN + EXECUTE [dbo].[proc_UserHistoryAttributeSave] null, + @UserHistoryId, + @detailInfoAttributeId, + NULL, + @detailedInfo, -- textValue, + NULL, -- booleanValue, + NULL, -- dateValue, + 0, -- deleted + @amendUserId, + @amendDate + END + + -- User Agent + IF @userAgentAttributeId > 0 AND @userAgent IS NOT NULL + BEGIN + + EXECUTE [dbo].[proc_UserHistoryAttributeSave] null, + @UserHistoryId, + @userAgentAttributeId, + NULL, -- intValue + @userAgent, -- textValue, + NULL, -- booleanValue, + NULL, -- dateValue, + 0, -- deleted + @amendUserId, + @amendDate + END + + -- Browser Name + IF @browserNameAttributeId > 0 AND @browserName IS NOT NULL + BEGIN + + EXECUTE [dbo].[proc_UserHistoryAttributeSave] null, + @UserHistoryId, + @browserNameAttributeId, + NULL, -- intValue + @browserName, -- textValue, + NULL, -- booleanValue, + NULL, -- dateValue, + 0, -- deleted + @amendUserId, + @amendDate + + END + + -- Browser Version + IF @browserVersionAttributeId > 0 AND @browserVersion IS NOT NULL + BEGIN + + + EXECUTE [dbo].[proc_UserHistoryAttributeSave] null, + @UserHistoryId, + @browserVersionAttributeId, + NULL, -- intValue + @browserVersion, + NULL, -- booleanValue, + NULL, -- dateValue, + 0, -- deleted + @amendUserId, + @amendDate + END + + + -- Url Referer + IF @urlRefererAttributeId > 0 AND @urlReferer IS NOT NULL + BEGIN + + EXECUTE [dbo].[proc_UserHistoryAttributeSave] null, + @UserHistoryId, + @urlRefererAttributeId, + NULL, -- intValue + @urlReferer, -- textValue, + NULL, -- booleanValue, + NULL, -- dateValue, + 0, -- deleted + @amendUserId, + @amendDate + + END + + + -- Login SuccessFul + IF @loginSuccessFulAttributeId > 0 AND @loginSuccessFul IS NOT NULL + BEGIN + + EXECUTE [dbo].[proc_UserHistoryAttributeSave] null, + @UserHistoryId, + @loginSuccessFulAttributeId, + NULL, -- intValue + @loginIP, -- textValue, + @loginSuccessFul, -- booleanValue, + NULL, -- dateValue, + 0, -- deleted + @amendUserId, + @amendDate + + END + + +END +GO + + +-------------------------------------------------------------------------------- +-- Swapnamol Abraham 01 Apr 2024 Initial Revision - services LH active user list +-------------------------------------------------------------------------------- + +CREATE PROCEDURE [dbo].[proc_ActiveLearningHubUserbyId] + @userId int +AS + DECLARE @History Table(userHistoryId int) + + DECLARE @ItemsReturned INT + DECLARE @StartRow INT + DECLARE @EndRow INT + + INSERT INTO @History + SELECT + userHistoryId + FROM + userHistoryTBL + WHERE + userId = @userId + and tenantId = 10 + AND isActive = 1 + ORDER BY + createdDate DESC + + + SELECT + uh.*, + uht.[Description], + ISNULL(t.tenantName,'Unknown') as tenantName + FROM userHistoryVW uh + INNER JOIN @History h ON h.userHistoryId = uh.userHistoryId + INNER JOIN userHistoryTypeTBL uht ON uht.UserHistoryTypeId = uh.userHistoryTypeId + LEFT JOIN tenantTBL t ON t.tenantId = uh.tenantId