diff --git a/.gitignore b/.gitignore index 8cc7d8bbe..47677a4a5 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,10 @@ obj /AdminUI/LearningHub.Nhs.AdminUI/web.config /LearningHub.Nhs.WebUI/web.config /WebAPI/LearningHub.Nhs.API/web.config +/LearningHub.Nhs.WebUI/nuget.config +/LearningHub.Nhs.WebUI.BlazorClient/Properties/launchSettings.json +/LearningHub.Nhs.WebUI.BlazorClient/wwwroot/appsettings.json +/LearningHub.Nhs.WebUI.BlazorClient/wwwroot/appsettings.Development.json +/LearningHub.Nhs.WebUI.BlazorClient/nuget.config +/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj.user +/nuget.config diff --git a/LearningHub.Nhs.Shared/Configuration/ExposableFindwiseSettings.cs b/LearningHub.Nhs.Shared/Configuration/ExposableFindwiseSettings.cs new file mode 100644 index 000000000..0b915bdd7 --- /dev/null +++ b/LearningHub.Nhs.Shared/Configuration/ExposableFindwiseSettings.cs @@ -0,0 +1,29 @@ +namespace LearningHub.Nhs.Shared.Configuration +{ + using LearningHub.Nhs.Shared.Interfaces.Configuration; + /// + /// Represents a public-facing set of configuration values for Findwise search, + /// intended to be safely exposed to client-side applications or public APIs. + /// + /// + /// Contains only non-sensitive data such as page sizes for various search types. + /// + /// + public class ExposableFindwiseSettings : IExposableFindwiseSettings + { + /// + /// Gets or sets the ResourceSearchPageSize. + /// + public int ResourceSearchPageSize { get; set; } + + /// + /// Gets or sets the CatalogueSearchPageSize. + /// + public int CatalogueSearchPageSize { get; set; } + + /// + /// Gets or sets the AllCatalogueSearchPageSize. + /// + public int AllCatalogueSearchPageSize { get; set; } + } +} diff --git a/LearningHub.Nhs.Shared/Configuration/ExposableSettings.cs b/LearningHub.Nhs.Shared/Configuration/ExposableSettings.cs new file mode 100644 index 000000000..25a82505b --- /dev/null +++ b/LearningHub.Nhs.Shared/Configuration/ExposableSettings.cs @@ -0,0 +1,36 @@ +namespace LearningHub.Nhs.Shared.Configuration +{ + using LearningHub.Nhs.Shared.Interfaces.Configuration; + /// + /// Represents configuration values that are safe to expose to clientside frontend applications + /// (such as Blazor WebAssembly) or public-facing APIs. + /// + /// + /// Implements and contains only non-sensitive, non-secret + /// values such as public API endpoints and pagination settings. This separation ensures + /// that secure or private configuration data is not inadvertently exposed to clients. + /// + /// + public class ExposableSettings : IExposableSettings + { + /// + public string LearningHubApiUrl { get; set; } + + /// + /// Gets or sets the UserApiUrl. + /// + public string UserApiUrl { get; set; } + + /// + /// Gets or sets the OpenApiUrl. + /// + public string OpenApiUrl { get; set; } + /// + /// Backend for Frontend (BFF) URL for the Learning Hub API accessed by samesite cookie and uses httpclients with bearers to access external apis. + /// + public string LearningHubApiBFFUrl { get; set; } + /// + public IExposableFindwiseSettings FindwiseSettings { get; set; } + + } +} diff --git a/LearningHub.Nhs.Shared/Helpers/gitIncludeFolderStructureForFuture.txt b/LearningHub.Nhs.Shared/Helpers/gitIncludeFolderStructureForFuture.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/LearningHub.Nhs.Shared/Helpers/gitIncludeFolderStructureForFuture.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableFindwiseSettings.cs b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableFindwiseSettings.cs new file mode 100644 index 000000000..3c3a9691a --- /dev/null +++ b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableFindwiseSettings.cs @@ -0,0 +1,29 @@ +namespace LearningHub.Nhs.Shared.Interfaces.Configuration +{ + /// + /// Represents configuration values related to Findwise search that are safe to expose + /// to client-side applications or public-facing APIs. + /// + /// + /// This includes non-sensitive values such as page sizes for different types of search results. + /// It does not contain any secure credentials or internal service configuration. + /// + /// + public interface IExposableFindwiseSettings + { + /// + /// Gets or sets the page size for resource search results. + /// + public int ResourceSearchPageSize { get; set; } + + /// + /// Gets or sets the CatalogueSearchPageSize. + /// + public int CatalogueSearchPageSize { get; set; } + + /// + /// Gets or sets the AllCatalogueSearchPageSize. + /// + public int AllCatalogueSearchPageSize { get; set; } + } +} diff --git a/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableSettings.cs b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableSettings.cs new file mode 100644 index 000000000..753858fba --- /dev/null +++ b/LearningHub.Nhs.Shared/Interfaces/Configuration/IExposableSettings.cs @@ -0,0 +1,42 @@ +namespace LearningHub.Nhs.Shared.Interfaces.Configuration +{ + /// + /// Defines a contract for configuration data that is non-sensitive and safe to expose publicly + /// + /// + /// This interface exposes only data that is safe to be publicly consumed or shared, + /// such as API endpoint URLs or non-sensitive configuration values. + /// It explicitly excludes any private or sensitive information (e.g., authentication tokens, + /// credentials, or secret keys), which should be handled via separate interfaces or services. + /// + /// + /// + /// The data provided by this interface can be safely used in frontend technologies, + /// such as Blazor WebAssembly, JavaScript frameworks, or other client-side applications, + /// without risking exposure of sensitive information. + /// + /// + public interface IExposableSettings + { + /// + /// Gets or sets the LearningHubApiUrl. + /// + public string LearningHubApiUrl { get; set; } + + /// + /// Gets or sets the UserApiUrl. + /// + public string UserApiUrl { get; set; } + + /// + /// Gets or sets the OpenApiUrl. + /// + public string OpenApiUrl { get; set; } + /// + /// Gets or sets the LearningHubApiBFFUrl used to proxy via same domain cookie to the BFF LearningHubAPI calls. + /// + public string LearningHubApiBFFUrl { get; set; } + + public IExposableFindwiseSettings FindwiseSettings { get; set; } + } +} diff --git a/LearningHub.Nhs.Shared/Interfaces/Http/IAPIHttpClient.cs b/LearningHub.Nhs.Shared/Interfaces/Http/IAPIHttpClient.cs new file mode 100644 index 000000000..c65fe674b --- /dev/null +++ b/LearningHub.Nhs.Shared/Interfaces/Http/IAPIHttpClient.cs @@ -0,0 +1,18 @@ +namespace LearningHub.Nhs.Shared.Interfaces.Http +{ + /// + /// Represents an HTTP client for a specific API. + /// + public interface IAPIHttpClient + { + /// + /// Gets the configured for the API. + /// + Task GetClientAsync(); + + /// + /// Gets the base URL of the API. + /// + string ApiUrl { get; } + } +} diff --git a/LearningHub.Nhs.Shared/Interfaces/Http/ILearningHubHttpClient.cs b/LearningHub.Nhs.Shared/Interfaces/Http/ILearningHubHttpClient.cs new file mode 100644 index 000000000..68ebb245b --- /dev/null +++ b/LearningHub.Nhs.Shared/Interfaces/Http/ILearningHubHttpClient.cs @@ -0,0 +1,22 @@ +namespace LearningHub.Nhs.Shared.Interfaces.Http +{ + /// + /// Marker interface for the LearningHub API HttpClient. + /// + /// + /// Inherits from to enable + /// dependency injection of a specific implementation configured with + /// different API endpoints or settings specific to LH API. + /// + /// + /// + /// Currently, this interface is empty and used solely to differentiate implementations + /// that connect to different endpoints via configuration, but it may be extended in the future + /// with LearningHub-specific functionality or properties. + /// + /// + public interface ILearningHubHttpClient : IAPIHttpClient + { + + } +} diff --git a/LearningHub.Nhs.Shared/Interfaces/Http/IOpenAPIHttpClient.cs b/LearningHub.Nhs.Shared/Interfaces/Http/IOpenAPIHttpClient.cs new file mode 100644 index 000000000..cffdea50a --- /dev/null +++ b/LearningHub.Nhs.Shared/Interfaces/Http/IOpenAPIHttpClient.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LearningHub.Nhs.Shared.Interfaces.Http +{ + /// + /// Marker interface for the IOpenAPIHttpClient API HttpClient. + /// + /// + /// Inherits from to enable + /// dependency injection of a specific implementation configured with + /// a openapi-related API endpoint or settings. + /// + /// + /// + /// This interface is currently empty and used solely to differentiate + /// implementations that connect to different endpoints via configuration. + /// It may be extended in the future with user-specific functionality or properties. + /// + /// + public interface IOpenApiHttpClient : IAPIHttpClient + { + + } +} diff --git a/LearningHub.Nhs.Shared/Interfaces/Http/IUserAPIHttpClient.cs b/LearningHub.Nhs.Shared/Interfaces/Http/IUserAPIHttpClient.cs new file mode 100644 index 000000000..25ec29d70 --- /dev/null +++ b/LearningHub.Nhs.Shared/Interfaces/Http/IUserAPIHttpClient.cs @@ -0,0 +1,22 @@ +namespace LearningHub.Nhs.Shared.Interfaces.Http +{ + /// + /// Marker interface for the User API HttpClient. + /// + /// + /// Inherits from to enable + /// dependency injection of a specific implementation configured with + /// a user-related API endpoint or settings. + /// + /// + /// + /// This interface is currently empty and used solely to differentiate + /// implementations that connect to different endpoints via configuration. + /// It may be extended in the future with user-specific functionality or properties. + /// + /// + public interface IUserApiHttpClient : IAPIHttpClient + { + + } +} diff --git a/LearningHub.Nhs.Shared/LearningHub.Nhs.Shared.csproj b/LearningHub.Nhs.Shared/LearningHub.Nhs.Shared.csproj new file mode 100644 index 000000000..d51462771 --- /dev/null +++ b/LearningHub.Nhs.Shared/LearningHub.Nhs.Shared.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/LearningHub.Nhs.Shared/Models/gitIncludeFolderStructureForFuture.txt b/LearningHub.Nhs.Shared/Models/gitIncludeFolderStructureForFuture.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/LearningHub.Nhs.Shared/Models/gitIncludeFolderStructureForFuture.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LearningHub.Nhs.Shared/Services/gitIncludeFolderStructureForFuture.txt b/LearningHub.Nhs.Shared/Services/gitIncludeFolderStructureForFuture.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/LearningHub.Nhs.Shared/Services/gitIncludeFolderStructureForFuture.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/DI/DI.cs b/LearningHub.Nhs.WebUI.BlazorClient/DI/DI.cs new file mode 100644 index 000000000..3be1a1cb2 --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/DI/DI.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using LearningHub.Nhs.Shared.Configuration; + + +namespace LearningHub.Nhs.WebUI.BlazorClient.DI +{ + public static class DI + { + public static IHttpClientBuilder AddBffHttpClient(this IServiceCollection services, Func getApiUrl) + where TInterface : class + where TImplementation : class, TInterface + { + return services.AddHttpClient((serviceProvider, client) => + { + var ExposableSettings = serviceProvider.GetRequiredService>().Value; + var apiUrl = getApiUrl(ExposableSettings); + var apiUri = new Uri(apiUrl); + var apiHost = apiUri.Host; + string forwardSlash = "/"; + // Using the Uri class for robust path joining + client.BaseAddress = new Uri($"{ExposableSettings.LearningHubApiBFFUrl}{apiHost}{forwardSlash}"); + }); + } + } +} diff --git a/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj b/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj new file mode 100644 index 000000000..52aa0dbbb --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/LearningHub.Nhs.WebUI.BlazorClient.csproj @@ -0,0 +1,44 @@ + + + + net8.0 + enable + enable + + + + true + full + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/Program.cs b/LearningHub.Nhs.WebUI.BlazorClient/Program.cs new file mode 100644 index 000000000..21f83b13d --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/Program.cs @@ -0,0 +1,98 @@ +using Blazored.LocalStorage; +using LearningHub.Nhs.WebUI.BlazorClient.DI; +using LearningHub.Nhs.Shared.Configuration; +using LearningHub.Nhs.Shared.Interfaces.Http; +using LearningHub.Nhs.WebUI.BlazorClient.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +// Serilog core (used via appsettings, do not delete even if vs marks not in use) +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +// Serilog extensions and sinks (used via appsettings, do not delete even if vs marks not in use) +using Serilog.Extensions.Logging; +using Serilog.Formatting.Compact; +using Serilog.Settings.Configuration; +using Serilog.Sinks.BrowserConsole; +using System; +using TELBlazor.Components.Core.Configuration; +using TELBlazor.Components.Core.Services.HelperServices; +using TELBlazor.Components.OptionalImplementations.Core.Services.HelperServices; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +var http = new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }; +var env = builder.HostEnvironment.Environment; +using var envSettings = await http.GetStreamAsync($"appsettings.{env}.json"); + +builder.Configuration.AddJsonStream(envSettings); + +builder.Services.Configure(builder.Configuration.GetSection("Settings")); +builder.Logging.ClearProviders(); + +// Read default logging level from configuration +var logLevelString = builder.Configuration["Serilog:MinimumLevel:Default"]; +// Convert string to LogEventLevel (with fallback) +if (!Enum.TryParse(logLevelString, true, out LogEventLevel defaultLogLevel)) +{ + defaultLogLevel = LogEventLevel.Information; // Default if parsing fails +} + +// Create a LoggingLevelSwitch that can be updated dynamically +LoggingLevelSwitch levelSwitch = new LoggingLevelSwitch(defaultLogLevel); // Default: Information added this so in production can change the logging +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .MinimumLevel.ControlledBy(levelSwitch) + .CreateLogger(); + +// Add Serilog to logging providers +builder.Logging.AddSerilog(Log.Logger); + +//for really bad fails +try +{ + // Candidates for DI collection + builder.Services.AddSingleton(sp => + { + return new TELBlazorBaseComponentConfiguration + { + JSEnabled = true, //if we are inject the client then it is true + HostType = $"{builder.Configuration["Properties:Environment"]} {builder.Configuration["Properties:Application"]}" + }; + }); + + builder.Services.AddBlazoredLocalStorage(); + + + // Register BFF using httpclient ILearningHubHttpClient + builder.Services.AddBffHttpClient(settings => settings.LearningHubApiUrl); + + // Register BFF using httpclient IUserApiHttpClient + builder.Services.AddBffHttpClient(settings => settings.UserApiUrl); + + // Register BFF using httpclient IOpenApiHttpClient + builder.Services.AddBffHttpClient(settings => settings.OpenApiUrl); + + + builder.Services.AddScoped(sp => levelSwitch); + builder.Services.AddScoped(); + + // TODO (QQQQ) implement post TD-5925 builder.Services.AddScoped(); + + await builder.Build().RunAsync(); +} +catch (Exception ex) +{ + //If in production as requires sending to api we may never receive it + Log.Fatal(ex, "Application terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); // Ensure logs are flushed before exit +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/Services/GenericAPIHttpClient.cs b/LearningHub.Nhs.WebUI.BlazorClient/Services/GenericAPIHttpClient.cs new file mode 100644 index 000000000..f08a7c09c --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/Services/GenericAPIHttpClient.cs @@ -0,0 +1,30 @@ +using LearningHub.Nhs.Shared.Interfaces.Http; + +namespace LearningHub.Nhs.WebUI.BlazorClient.Services +{ + public class GenericAPIHttpClient : IAPIHttpClient, ILearningHubHttpClient, IUserApiHttpClient, IOpenApiHttpClient + { + private readonly HttpClient _httpClient; // Private field to hold the injected HttpClient + + /// + /// Initializes a new instance of the class. + /// + /// The HttpClient instance provided by dependency injection. + public GenericAPIHttpClient(HttpClient httpClient) // Inject HttpClient + { + _httpClient = httpClient; + } + + public string ApiUrl => _httpClient.BaseAddress.AbsoluteUri; + + /// + /// Retrieves the configured HttpClient instance. + /// + /// A Task that resolves to the HttpClient instance. + public Task GetClientAsync() + { + // Return the injected HttpClient instance wrapped in a completed Task + return Task.FromResult(_httpClient); + } + } +} diff --git a/LearningHub.Nhs.WebUI.BlazorClient/Services/WasmCacheServiceStub.cs b/LearningHub.Nhs.WebUI.BlazorClient/Services/WasmCacheServiceStub.cs new file mode 100644 index 000000000..6083581c2 --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/Services/WasmCacheServiceStub.cs @@ -0,0 +1,62 @@ +// TODO (QQQQ) implement post TD-5925: using LearningHub.Nhs.Models; + +namespace LearningHub.Nhs.WebUI.BlazorClient.Services +{ + /// + /// We may use storage, we may just stub it and throw an error, we cant directly use redis we may access it via an api + /// The cachestub currently just returns there is nothing available so the caller then will revert to calling the api. + /// + public class WasmCacheServiceStub // TODO (QQQQ) implement post TD-5925 and get from NHS.Models: ICacheService + { + public Task GetAsync(string key) + { + return Task.FromResult(default(T)); + } + + public Task<(bool Success, T Value)> TryGetAsync(string key) + { + return Task.FromResult((false, default(T))); + } + + public Task SetAsync(string key, T value) + { + return Task.FromResult(value); + } + + public Task RemoveAsync(string key) + { + return Task.CompletedTask; + } + + public Task SetAsync(string key, T value, int? expiryInMinutes, bool slidingExpiration = true) + { + return Task.FromResult(value); + } + + public Task GetOrCreateAsync(string key, Func getValue) + { + return Task.FromResult(getValue()); + } + + public Task GetOrCreateAsync(string key, Func getValue, int? expiryInMinutes, bool slidingExpiration = true) + { + return Task.FromResult(getValue()); + } + + public Task GetOrFetchAsync(string key, Func> getValue) + { + return getValue(); + } + + public Task GetOrFetchAsync(string key, Func> getValue, int? expiryInMinutes, bool slidingExpiration = true) + { + return getValue(); + } + + public Task FlushAll() + { + return Task.CompletedTask; + } + + } +} diff --git a/LearningHub.Nhs.WebUI.BlazorClient/_Imports.razor b/LearningHub.Nhs.WebUI.BlazorClient/_Imports.razor new file mode 100644 index 000000000..1289a8aff --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using LearningHub.Nhs.WebUI.BlazorClient \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.BlazorClient/wwwroot/appsettings.json b/LearningHub.Nhs.WebUI.BlazorClient/wwwroot/appsettings.json new file mode 100644 index 000000000..180fa667a --- /dev/null +++ b/LearningHub.Nhs.WebUI.BlazorClient/wwwroot/appsettings.json @@ -0,0 +1,55 @@ +{ + "Settings": { + "BuildNumber": "Production", + "https": null, + "UserApiUrl": "https://learninghubnhsuk-userapi-prod.azurewebsites.net/api/", + "OpenApiUrl": "https://learninghubnhsuk-openapi-prod.azurewebsites.net/api/", + "LearningHubApiUrl": "https://learninghub.nhs.uk/api/", + "LearningHubApiBFFUrl": "https://learninghub.nhs.uk/bff/", + "LearningHubWebUiUrl": "https://learninghub.nhs.uk/", + "LearningHubAdminUrl": "https://auth.learninghub.nhs.uk/" + }, + "SocialMediaSharingUrls": { + "Facebook": "https://www.facebook.com/sharer.php?u=", + "Twitter": "https://twitter.com/intent/tweet?url=", + "LinkedIn": "https://www.linkedin.com/sharing/share-offsite/?url=" + }, + "FindwiseSettings": { + "ResourceSearchPageSize": 10, + "CatalogueSearchPageSize": 3, + "AllCatalogueSearchPageSize": 10 + }, + + "APIs": { + }, + + "Serilog": { + "Using": [ + "Serilog.Sinks.BrowserConsole" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "BrowserConsole", + "Args": { + "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], + "Properties": { + "Application": "LearningHub Nhs WebUI Blazor Client", + "Environment": "Production" + } + }, + "Properties": { + "Application": "LearningHub Nhs WebUI Blazor Client", + "Environment": "Production" + } +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.Shared/Helpers/gitIncludeFolderStructureForFuture.txt b/LearningHub.Nhs.WebUI.Shared/Helpers/gitIncludeFolderStructureForFuture.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/LearningHub.Nhs.WebUI.Shared/Helpers/gitIncludeFolderStructureForFuture.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.Shared/Interfaces/gitIncludeFolderStructureForFuture.txt b/LearningHub.Nhs.WebUI.Shared/Interfaces/gitIncludeFolderStructureForFuture.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/LearningHub.Nhs.WebUI.Shared/Interfaces/gitIncludeFolderStructureForFuture.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.Shared/LearningHub.Nhs.WebUI.Shared.csproj b/LearningHub.Nhs.WebUI.Shared/LearningHub.Nhs.WebUI.Shared.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/LearningHub.Nhs.WebUI.Shared/LearningHub.Nhs.WebUI.Shared.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/LearningHub.Nhs.WebUI.Shared/Models/gitIncludeFolderStructureForFuture.txt b/LearningHub.Nhs.WebUI.Shared/Models/gitIncludeFolderStructureForFuture.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/LearningHub.Nhs.WebUI.Shared/Models/gitIncludeFolderStructureForFuture.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.Shared/Services/gitIncludeFolderStructureForFuture.txt b/LearningHub.Nhs.WebUI.Shared/Services/gitIncludeFolderStructureForFuture.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/LearningHub.Nhs.WebUI.Shared/Services/gitIncludeFolderStructureForFuture.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.sln b/LearningHub.Nhs.WebUI.sln index 55cce01f0..0dcc460c7 100644 --- a/LearningHub.Nhs.WebUI.sln +++ b/LearningHub.Nhs.WebUI.sln @@ -4,11 +4,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LearningHub.Nhs.WebUI", "LearningHub.Nhs.WebUI\LearningHub.Nhs.WebUI.csproj", "{16BBF937-C1E9-4240-B56E-20E3E5FA2005}" + ProjectSection(ProjectDependencies) = postProject + {9F1B0470-E809-49FE-A6E8-152C7EBD012E} = {9F1B0470-E809-49FE-A6E8-152C7EBD012E} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B5D48B6A-D4A7-494E-89C0-64428232D242}" ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props - Directory.Packages.props = Directory.Packages.props + .editorconfig = .editorconfig + global.json = global.json + nuget.config = nuget.config + nuget.config.cicd = nuget.config.cicd + nuget.config.template = nuget.config.template StyleCop.ruleset = StyleCop.ruleset EndProjectSection EndProject @@ -17,6 +23,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MigrationTool", "MigrationTool", "{94676CCE-A38B-4FAF-905E-CE85CE95845E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LearningHub.Nhs.Api", "WebAPI\LearningHub.Nhs.API\LearningHub.Nhs.Api.csproj", "{21F15E96-314F-4F39-822F-C2568CDC4A5A}" + ProjectSection(ProjectDependencies) = postProject + {9F1B0470-E809-49FE-A6E8-152C7EBD012E} = {9F1B0470-E809-49FE-A6E8-152C7EBD012E} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LearningHub.Nhs.Api.Shared", "WebAPI\LearningHub.Nhs.Api.Shared\LearningHub.Nhs.Api.Shared.csproj", "{719833B9-EC79-48F2-9123-C4DF111AE9AA}" EndProject @@ -51,6 +60,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AdminUI", "AdminUI", "{9642BC19-BAE7-45A9-B4F2-8D7529786CDC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LearningHub.Nhs.AdminUI", "AdminUI\LearningHub.Nhs.AdminUI\LearningHub.Nhs.AdminUI.csproj", "{1C97A3C2-73E8-4AFF-92EF-F65B4899FADB}" + ProjectSection(ProjectDependencies) = postProject + {9F1B0470-E809-49FE-A6E8-152C7EBD012E} = {9F1B0470-E809-49FE-A6E8-152C7EBD012E} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenAPI", "OpenAPI", "{66ED23A2-F15A-4ECB-A84D-736C95BEFC61}" EndProject @@ -82,6 +94,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LearningHub.Nhs.ReportApi.S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.WebUI.AutomatedUiTests", "LearningHub.Nhs.WebUI.AutomatedUiTests\LearningHub.Nhs.WebUI.AutomatedUiTests.csproj", "{A84EC50B-2B01-4819-A2B1-BD867B7595CA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.Shared", "LearningHub.Nhs.Shared\LearningHub.Nhs.Shared.csproj", "{9F1B0470-E809-49FE-A6E8-152C7EBD012E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.WebUI.BlazorClient", "LearningHub.Nhs.WebUI.BlazorClient\LearningHub.Nhs.WebUI.BlazorClient.csproj", "{A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LearningHub.Nhs.WebUI.Shared", "LearningHub.Nhs.WebUI.Shared\LearningHub.Nhs.WebUI.Shared.csproj", "{22596C8C-EF3A-4046-BB85-012D851D8D16}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -346,6 +364,30 @@ Global {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Release|Any CPU.Build.0 = Release|Any CPU {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Release|x64.ActiveCfg = Release|Any CPU {A84EC50B-2B01-4819-A2B1-BD867B7595CA}.Release|x64.Build.0 = Release|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Debug|x64.Build.0 = Debug|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Release|Any CPU.Build.0 = Release|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Release|x64.ActiveCfg = Release|Any CPU + {9F1B0470-E809-49FE-A6E8-152C7EBD012E}.Release|x64.Build.0 = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Debug|x64.Build.0 = Debug|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|Any CPU.Build.0 = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|x64.ActiveCfg = Release|Any CPU + {A7DA82FE-A46C-47E9-8BD6-7FD7A8376CBA}.Release|x64.Build.0 = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|x64.ActiveCfg = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Debug|x64.Build.0 = Debug|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|Any CPU.Build.0 = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|x64.ActiveCfg = Release|Any CPU + {22596C8C-EF3A-4046-BB85-012D851D8D16}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LearningHub.Nhs.WebUI/BlazorPageHosting/App.razor b/LearningHub.Nhs.WebUI/BlazorPageHosting/App.razor new file mode 100644 index 000000000..884020c95 --- /dev/null +++ b/LearningHub.Nhs.WebUI/BlazorPageHosting/App.razor @@ -0,0 +1 @@ +@* No-op App component *@ diff --git a/LearningHub.Nhs.WebUI/Configuration/BFFPathValidationOptions.cs b/LearningHub.Nhs.WebUI/Configuration/BFFPathValidationOptions.cs new file mode 100644 index 000000000..3d0d490af --- /dev/null +++ b/LearningHub.Nhs.WebUI/Configuration/BFFPathValidationOptions.cs @@ -0,0 +1,25 @@ +namespace LearningHub.Nhs.WebUI.Configuration +{ + using System.Collections.Generic; + + /// + /// Configuration options for validating BFF paths. + /// + public class BFFPathValidationOptions + { + /// + /// Gets the section name for BFF path validation options. + /// + public const string SectionName = "BFFPathValidation"; + + /// + /// Gets or sets which apis and api stems we are allowing. + /// + public List AllowedPathPrefixes { get; set; } = new List(); + + /// + /// Gets or sets fine tuning of what paths the BFF can be used to access and what not to, where we want to specifically protect against something. + /// + public List BlockedPathSegments { get; set; } = new List(); + } +} diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index e300e80e3..b8a26f73b 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -1,12 +1,14 @@ namespace LearningHub.Nhs.WebUI.Configuration { using System; + using LearningHub.Nhs.Shared.Configuration; + using LearningHub.Nhs.Shared.Interfaces.Configuration; using LearningHub.Nhs.WebUI.Models.Contribute; /// /// Defines the . /// - public class Settings + public class Settings : IExposableSettings { /// /// Initializes a new instance of the class. @@ -31,6 +33,9 @@ public Settings() /// public string LearningHubApiUrl { get; set; } + /// + public string LearningHubApiBFFUrl { get; set; } + /// /// Gets or sets the OpenApiUrl. /// @@ -254,7 +259,7 @@ public Settings() /// /// Gets or sets the FindwiseSettings. /// - public FindwiseSettings FindwiseSettings { get; set; } = new FindwiseSettings(); + public IExposableFindwiseSettings FindwiseSettings { get; set; } = new ExposableFindwiseSettings(); /// /// Gets or sets the MediaKindSettings. diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/BFFController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/BFFController.cs new file mode 100644 index 000000000..c5f082d75 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Controllers/Api/BFFController.cs @@ -0,0 +1,245 @@ +namespace LearningHub.Nhs.WebUI.Controllers.Api +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading.Tasks; + using LearningHub.Nhs.Shared.Interfaces.Http; + using LearningHub.Nhs.WebUI.Configuration; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + /// + /// This controller allows proxying of requests to different APIs using same site cookie authentication. + /// It uses the http clients registered in the DI container. + /// The BFF (Backend for Frontend) pattern is used to simplify client-side code and centralize API access, application services directly call external apis currently but they could use the bff and then introduce caching there too potentially for seperation of infastructure concerns. + /// Unauthorized requests will be redirected to the login page so 302s are expected when unauthorized, and redirecting for using a Blazor island component for example may not be desireable so these responses need to be handled by the caller. + /// This controller is designed to be used with a clientside calls i.e. Blazor utilizing the BFF pattern, which enables same site cookie authentication and avoid the necessity of storing tokens in client storage + /// The bff prefix is followed by the API name (e.g. "learninghub", "userapi") and the path to the specific endpoint to enable easy routing to different APIs. + /// See confluence for more details on the BFF pattern and how to use this controller. + /// + /// The authorize same site cookie is used for security between client and server. API calls relying on policys such as AuthorizeOrCallFromLH may not be proxied as they require the Authorization header to be present. + [Authorize] + [Route("bff/{apiName}/{**path}")] + [ApiController] + public class BFFController : BaseApiController + { + private readonly IOptions bffPathValidationOptions; + + /// + /// The list of API clients that can be used to proxy requests. + /// + private List apiClients; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance used for logging. + /// The HTTP client for the Learning Hub API. + /// The HTTP client for the User API. + /// The HTTP client for the Open API. + /// The options for validating BFF paths. + public BFFController( + ILogger logger, + ILearningHubHttpClient learningHubClient, + IUserApiHttpClient userAPIClient, + IOpenApiHttpClient openAPIClient, + IOptions bffPathValidationOptions) + : base(logger) + { + // Clients the BFF is being given access to, these are the only clients that can be used to proxy requests. + this.apiClients = new List() + { + learningHubClient, + userAPIClient, + openAPIClient, + }; + + this.bffPathValidationOptions = bffPathValidationOptions; + } + + /// + /// Takes an API name and a path, and proxies the request to the appropriate API provided that api is part of the client list and the path is allowed and not blocked. + /// + /// The name of the API to which the request should be proxied. + /// The path of the endpoint within the specified API. + /// An representing the result of the proxied request. + [HttpGet] + [HttpPost] + [HttpPut] + [HttpDelete] + [HttpPatch] + public async Task ProxyRequest(string apiName, string path) + { + string sanitizedPath = path?.Trim('/').ToLowerInvariant() ?? string.Empty; + string sanitizedApiName = apiName?.Trim('/').ToLowerInvariant() ?? string.Empty; + + IAPIHttpClient apiClient; + try + { + apiClient = this.apiClients.Single(x => + { + try + { + var uri = new Uri(x.ApiUrl); + return uri.Host.ToLowerInvariant() == sanitizedApiName; + } + catch + { + return false; + } + }); + } + catch (Exception e) + { + this.Logger.LogError(e, "Failed to find API client for {ApiName}", sanitizedApiName); + return this.BadRequest($"Unknown API alias: {sanitizedApiName}"); + } + + if (!this.IsPathAllowed(sanitizedPath)) + { + return this.Forbid("This path is not allowed via BFF proxy."); + } + + var client = await apiClient.GetClientAsync(); + string targetUrl = $"{apiClient.ApiUrl.TrimEnd('/')}/{path}"; + + // Add query parameters from the original request + if (this.Request.QueryString.HasValue) + { + targetUrl += this.Request.QueryString.Value; + } + + /* + No headers for Auth, host, connection, user agent, added becaue all security is handled by serverside httpclients via baseclient + BaseHttpClient should handle content-type, timezone and tokens. + Note: We do not forward the Authorization header as the BFF pattern uses same-site cookies for authentication. + This means the BFF controller is responsible for handling authentication and authorization. + We also do not forward the Host header as it may not match the target API's expected host. + Header copying would only be needed if: APIs start checking for custom client headers (X-Custom-Header, X-Correlation-Id, etc.) + */ + + // Copy body if necessary (for POST, PUT, PATCH, etc.) + var method = new HttpMethod(this.Request.Method); + var requestMessage = new HttpRequestMessage(method, targetUrl); + + if (this.Request.ContentLength > 0 && + !string.Equals(this.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) && + !string.Equals(this.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase)) + { + requestMessage.Content = new StreamContent(this.Request.Body); + if (!string.IsNullOrEmpty(this.Request.ContentType)) + { + requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue(this.Request.ContentType); + } + } + + try + { + var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); + + // Handle redirects with token preservation + if (response.StatusCode == System.Net.HttpStatusCode.Redirect || + response.StatusCode == System.Net.HttpStatusCode.Found || + response.StatusCode == System.Net.HttpStatusCode.TemporaryRedirect || + response.StatusCode == System.Net.HttpStatusCode.PermanentRedirect) + { + return await this.HandleRedirect(response, apiClient); + } + + var content = await response.Content.ReadAsStringAsync(); + var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/json"; + + return new ContentResult + { + Content = content, + ContentType = contentType, + StatusCode = (int)response.StatusCode, + }; + } + catch (HttpRequestException ex) + { + this.Logger.LogError(ex, "Error proxying request to {TargetUrl}", targetUrl); + return this.StatusCode(500, "An error occurred while processing the request."); + } + } + + /* + Handle redirects with token preservation + if we are redirected the client may not handle it as it isnt the token holder so we need to continue using the bff until we get the outcome + if the BFF caller is not expecting redirects but only data they should handle the 302 response and redirect themselves. + E.g. A compontent that uses the BFF to fetch data may not be appropriate for redirecting to a specific page so the consuming client may need to have a way of handling page redirects. + */ + private async Task HandleRedirect(HttpResponseMessage response, IAPIHttpClient apiClient) + { + var location = response.Headers.Location?.ToString(); + + if (string.IsNullOrEmpty(location)) + { + return this.StatusCode((int)response.StatusCode, "Redirect location not found"); + } + + // Check if the redirect location is relative or absolute + string redirectUrl; + if (Uri.IsWellFormedUriString(location, UriKind.Absolute)) + { + redirectUrl = location; + } + else + { + // Handle relative redirects + var baseUri = new Uri(apiClient.ApiUrl); + redirectUrl = new Uri(baseUri, location).ToString(); + } + + // Create a new request for the redirect + var redirectRequest = new HttpRequestMessage(HttpMethod.Get, redirectUrl); + + // Add authentication token to the redirect request (apiClient handles this) + // No additional headers needed - apiClient is already configured + try + { + var client = await apiClient.GetClientAsync(); + var redirectResponse = await client.SendAsync(redirectRequest); + var content = await redirectResponse.Content.ReadAsStringAsync(); + + // Our data apis are expected to return JSON, but we can handle other content types if necessary. + var contentType = redirectResponse.Content.Headers.ContentType?.MediaType ?? "application/json"; + + return new ContentResult + { + Content = content, + ContentType = contentType, + StatusCode = (int)redirectResponse.StatusCode, + }; + } + catch (HttpRequestException ex) + { + this.Logger.LogError(ex, "Error following redirect to {RedirectUrl}", redirectUrl); + return this.StatusCode(500, "An error occurred while following the redirect."); + } + } + + /// + /// Validates the path against allowed and blocked segments. + /// + private bool IsPathAllowed(string path) + { + var normalizedPath = path?.Trim('/').ToLowerInvariant() ?? string.Empty; + + // Check blacklist first + if (this.bffPathValidationOptions.Value.BlockedPathSegments.Any(blocked => normalizedPath.Contains(blocked.ToLowerInvariant()))) + { + this.Logger.LogError(" Black listed path {path} was requested and blocked", normalizedPath); + return false; + } + + // Check whitelist + return this.bffPathValidationOptions.Value.AllowedPathPrefixes.Any(prefix => normalizedPath.StartsWith(prefix.ToLowerInvariant())); + } + } +} diff --git a/LearningHub.Nhs.WebUI/Controllers/LogoutController.cs b/LearningHub.Nhs.WebUI/Controllers/LogoutController.cs index 99244e3ed..fb2b7d20e 100644 --- a/LearningHub.Nhs.WebUI/Controllers/LogoutController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/LogoutController.cs @@ -9,7 +9,7 @@ using IdentityModel; using IdentityModel.Client; - + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Handlers; using LearningHub.Nhs.WebUI.Interfaces; diff --git a/LearningHub.Nhs.WebUI/Helpers/LearningHubApiFacade.cs b/LearningHub.Nhs.WebUI/Helpers/LearningHubApiFacade.cs index adeb102a7..204333bba 100644 --- a/LearningHub.Nhs.WebUI/Helpers/LearningHubApiFacade.cs +++ b/LearningHub.Nhs.WebUI/Helpers/LearningHubApiFacade.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using LearningHub.Nhs.Models.Common; - using LearningHub.Nhs.WebUI.Interfaces; + using LearningHub.Nhs.Shared.Interfaces.Http; using Newtonsoft.Json; /// diff --git a/LearningHub.Nhs.WebUI/Helpers/OpenApiFacade.cs b/LearningHub.Nhs.WebUI/Helpers/OpenApiFacade.cs index 32c529a0d..3627bc4a8 100644 --- a/LearningHub.Nhs.WebUI/Helpers/OpenApiFacade.cs +++ b/LearningHub.Nhs.WebUI/Helpers/OpenApiFacade.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using LearningHub.Nhs.Models.Common; - using LearningHub.Nhs.WebUI.Interfaces; + using LearningHub.Nhs.Shared.Interfaces.Http; using Newtonsoft.Json; /// diff --git a/LearningHub.Nhs.WebUI/Interfaces/ILearningHubHttpClient.cs b/LearningHub.Nhs.WebUI/Interfaces/ILearningHubHttpClient.cs deleted file mode 100644 index d39113d99..000000000 --- a/LearningHub.Nhs.WebUI/Interfaces/ILearningHubHttpClient.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Interfaces -{ - using System.Net.Http; - using System.Threading.Tasks; - - /// - /// The LearningHubHttpClient interface. - /// - public interface ILearningHubHttpClient - { - /// - /// The get client. - /// - /// The . - Task GetClientAsync(); - } -} diff --git a/LearningHub.Nhs.WebUI/Interfaces/IOpenApiHttpClient.cs b/LearningHub.Nhs.WebUI/Interfaces/IOpenApiHttpClient.cs deleted file mode 100644 index 4c53a2c38..000000000 --- a/LearningHub.Nhs.WebUI/Interfaces/IOpenApiHttpClient.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Interfaces -{ - using System.Net.Http; - using System.Threading.Tasks; - - /// - /// The OpenApiHttpClient interface. - /// - public interface IOpenApiHttpClient - { - /// - /// The get client. - /// - /// The . - Task GetClientAsync(); - } -} diff --git a/LearningHub.Nhs.WebUI/Interfaces/IUserApiHttpClient.cs b/LearningHub.Nhs.WebUI/Interfaces/IUserApiHttpClient.cs deleted file mode 100644 index fb65ab6ee..000000000 --- a/LearningHub.Nhs.WebUI/Interfaces/IUserApiHttpClient.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Interfaces -{ - using System.Net.Http; - using System.Threading.Tasks; - - /// - /// The User Api HttpClient interface. - /// - public interface IUserApiHttpClient - { - /// - /// The get client. - /// - /// The . - Task GetClientAsync(); - } -} diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index dc2cf661d..f9a8aeb86 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -97,6 +97,9 @@ + + + @@ -104,6 +107,10 @@ + + + + @@ -167,6 +174,12 @@ + + + + + + diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs index c24d9057c..34013d03a 100644 --- a/LearningHub.Nhs.WebUI/Program.cs +++ b/LearningHub.Nhs.WebUI/Program.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using LearningHub.Nhs.WebUI; +using LearningHub.Nhs.WebUI.BlazorPageHosting; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.JsDetection; using LearningHub.Nhs.WebUI.Middleware; @@ -18,7 +19,6 @@ using tusdotnet; using tusdotnet.Models; using tusdotnet.Models.Configuration; - #pragma warning restore SA1200 // Using directives should be placed correctly var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger(); @@ -43,10 +43,13 @@ var appLifetime = app.Services.GetRequiredService(); var jsDetectionLogger = app.Services.GetRequiredService(); appLifetime.ApplicationStopping.Register(async () => await jsDetectionLogger.FlushCounters()); + app.UseBlazorFrameworkFiles(); + app.UseStaticFiles(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); + app.UseWebAssemblyDebugging(); } else { @@ -84,7 +87,6 @@ app.UseAuthorization(); app.UseMiddleware(); - app.UseStaticFiles(); app.Map(TimezoneInfoMiddleware.TimezoneInfoUrl, b => b.UseMiddleware()); @@ -108,6 +110,12 @@ }; }); + app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(LearningHub.Nhs.WebUI.BlazorClient._Imports).Assembly) + .AddAdditionalAssemblies(typeof(TELBlazor.Components._Imports).Assembly); + app.Run(); } catch (Exception ex) diff --git a/LearningHub.Nhs.WebUI/Services/ActivityService.cs b/LearningHub.Nhs.WebUI/Services/ActivityService.cs index 185dc3a2c..2840d3733 100644 --- a/LearningHub.Nhs.WebUI/Services/ActivityService.cs +++ b/LearningHub.Nhs.WebUI/Services/ActivityService.cs @@ -8,6 +8,7 @@ using LearningHub.Nhs.Models.Resource; using LearningHub.Nhs.Models.Resource.Activity; using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/BaseHttpClient.cs b/LearningHub.Nhs.WebUI/Services/BaseHttpClient.cs index a8ebda5ef..cb2900fc2 100644 --- a/LearningHub.Nhs.WebUI/Services/BaseHttpClient.cs +++ b/LearningHub.Nhs.WebUI/Services/BaseHttpClient.cs @@ -10,6 +10,7 @@ using IdentityModel.Client; using LearningHub.Nhs.Caching; using LearningHub.Nhs.Models.Extensions; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Extensions; using Microsoft.AspNetCore.Authentication; @@ -20,7 +21,7 @@ /// /// The abstract api http client. /// - public abstract class BaseHttpClient + public abstract class BaseHttpClient : IAPIHttpClient { private static readonly ConcurrentDictionary DictionaryLocks = new ConcurrentDictionary(); diff --git a/LearningHub.Nhs.WebUI/Services/BaseService.cs b/LearningHub.Nhs.WebUI/Services/BaseService.cs index ddcbad86f..f084de651 100644 --- a/LearningHub.Nhs.WebUI/Services/BaseService.cs +++ b/LearningHub.Nhs.WebUI/Services/BaseService.cs @@ -1,5 +1,6 @@ namespace LearningHub.Nhs.WebUI.Services { + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; diff --git a/LearningHub.Nhs.WebUI/Services/BoomarkService.cs b/LearningHub.Nhs.WebUI/Services/BoomarkService.cs index 15d5509f9..2d1a543d6 100644 --- a/LearningHub.Nhs.WebUI/Services/BoomarkService.cs +++ b/LearningHub.Nhs.WebUI/Services/BoomarkService.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using LearningHub.Nhs.Models.Bookmark; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/CardService.cs b/LearningHub.Nhs.WebUI/Services/CardService.cs index d3aac0616..6e75e66e8 100644 --- a/LearningHub.Nhs.WebUI/Services/CardService.cs +++ b/LearningHub.Nhs.WebUI/Services/CardService.cs @@ -6,6 +6,7 @@ namespace LearningHub.Nhs.WebUI.Services using System.Text; using System.Threading.Tasks; using LearningHub.Nhs.Models.Resource.ResourceDisplay; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/CatalogueService.cs b/LearningHub.Nhs.WebUI/Services/CatalogueService.cs index 3833a4e28..934e942c1 100644 --- a/LearningHub.Nhs.WebUI/Services/CatalogueService.cs +++ b/LearningHub.Nhs.WebUI/Services/CatalogueService.cs @@ -10,6 +10,7 @@ using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.User; using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/ContentService.cs b/LearningHub.Nhs.WebUI/Services/ContentService.cs index b6d1f9296..103579f6f 100644 --- a/LearningHub.Nhs.WebUI/Services/ContentService.cs +++ b/LearningHub.Nhs.WebUI/Services/ContentService.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; using LearningHub.Nhs.Models.Content; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/ContributeService.cs b/LearningHub.Nhs.WebUI/Services/ContributeService.cs index 68a2b4cf6..a447bd744 100644 --- a/LearningHub.Nhs.WebUI/Services/ContributeService.cs +++ b/LearningHub.Nhs.WebUI/Services/ContributeService.cs @@ -13,6 +13,7 @@ using LearningHub.Nhs.Models.Resource; using LearningHub.Nhs.Models.Resource.Contribute; using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models.Contribute; using Microsoft.AspNetCore.Http; @@ -27,6 +28,7 @@ public class ContributeService : BaseService, IContributeService { private readonly IAzureMediaService azureMediaService; + private readonly ILearningHubHttpClient learningHubHttpClient; private readonly IAzureMediaService mediaService; private readonly IFileService fileService; private readonly IResourceService resourceService; @@ -47,6 +49,7 @@ public ContributeService(IFileService fileService, IResourceService resourceServ this.fileService = fileService; this.resourceService = resourceService; this.azureMediaService = azureMediaService; + this.learningHubHttpClient = learningHubHttpClient; this.mediaService = mediaService; } diff --git a/LearningHub.Nhs.WebUI/Services/CountryService.cs b/LearningHub.Nhs.WebUI/Services/CountryService.cs index e1afd4b35..00b5fba00 100644 --- a/LearningHub.Nhs.WebUI/Services/CountryService.cs +++ b/LearningHub.Nhs.WebUI/Services/CountryService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; using elfhHub.Nhs.Models.Entities; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/DashboardService.cs b/LearningHub.Nhs.WebUI/Services/DashboardService.cs index 607d4f2be..549840117 100644 --- a/LearningHub.Nhs.WebUI/Services/DashboardService.cs +++ b/LearningHub.Nhs.WebUI/Services/DashboardService.cs @@ -10,6 +10,7 @@ using LearningHub.Nhs.Models.Entities.Analytics; using LearningHub.Nhs.Models.Entities.Reporting; using LearningHub.Nhs.Services.Interface; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models; using Microsoft.Extensions.Logging; diff --git a/LearningHub.Nhs.WebUI/Services/DetectJsLogService.cs b/LearningHub.Nhs.WebUI/Services/DetectJsLogService.cs index e9bf607ce..373ef016f 100644 --- a/LearningHub.Nhs.WebUI/Services/DetectJsLogService.cs +++ b/LearningHub.Nhs.WebUI/Services/DetectJsLogService.cs @@ -2,6 +2,7 @@ { using System; using System.Threading.Tasks; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; diff --git a/LearningHub.Nhs.WebUI/Services/GradeService.cs b/LearningHub.Nhs.WebUI/Services/GradeService.cs index 37df9b048..5a846a72b 100644 --- a/LearningHub.Nhs.WebUI/Services/GradeService.cs +++ b/LearningHub.Nhs.WebUI/Services/GradeService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/HierarchyService.cs b/LearningHub.Nhs.WebUI/Services/HierarchyService.cs index 6bed5fd27..895b6897d 100644 --- a/LearningHub.Nhs.WebUI/Services/HierarchyService.cs +++ b/LearningHub.Nhs.WebUI/Services/HierarchyService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.Hierarchy; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; diff --git a/LearningHub.Nhs.WebUI/Services/InternalSystemService.cs b/LearningHub.Nhs.WebUI/Services/InternalSystemService.cs index bb19a0f38..3322fab02 100644 --- a/LearningHub.Nhs.WebUI/Services/InternalSystemService.cs +++ b/LearningHub.Nhs.WebUI/Services/InternalSystemService.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; using LearningHub.Nhs.Models.Maintenance; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/JobRoleService.cs b/LearningHub.Nhs.WebUI/Services/JobRoleService.cs index 766d05e60..a2f879fda 100644 --- a/LearningHub.Nhs.WebUI/Services/JobRoleService.cs +++ b/LearningHub.Nhs.WebUI/Services/JobRoleService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models.Account; diff --git a/LearningHub.Nhs.WebUI/Services/LearningHubHttpClient.cs b/LearningHub.Nhs.WebUI/Services/LearningHubHttpClient.cs index 460ddabc3..9af95a1e6 100644 --- a/LearningHub.Nhs.WebUI/Services/LearningHubHttpClient.cs +++ b/LearningHub.Nhs.WebUI/Services/LearningHubHttpClient.cs @@ -2,6 +2,7 @@ { using System.Net.Http; using LearningHub.Nhs.Caching; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.Http; diff --git a/LearningHub.Nhs.WebUI/Services/LocationService.cs b/LearningHub.Nhs.WebUI/Services/LocationService.cs index 04423a0b6..415e3cd8f 100644 --- a/LearningHub.Nhs.WebUI/Services/LocationService.cs +++ b/LearningHub.Nhs.WebUI/Services/LocationService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/LoginWizardService.cs b/LearningHub.Nhs.WebUI/Services/LoginWizardService.cs index 06fd7e428..d1a46ff6e 100644 --- a/LearningHub.Nhs.WebUI/Services/LoginWizardService.cs +++ b/LearningHub.Nhs.WebUI/Services/LoginWizardService.cs @@ -9,6 +9,7 @@ using elfhHub.Nhs.Models.Entities; using elfhHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/MyLearningService.cs b/LearningHub.Nhs.WebUI/Services/MyLearningService.cs index 1e1662201..21155057d 100644 --- a/LearningHub.Nhs.WebUI/Services/MyLearningService.cs +++ b/LearningHub.Nhs.WebUI/Services/MyLearningService.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/NLogLogLevelSwitcherService.cs b/LearningHub.Nhs.WebUI/Services/NLogLogLevelSwitcherService.cs new file mode 100644 index 000000000..bae626eb1 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Services/NLogLogLevelSwitcherService.cs @@ -0,0 +1,97 @@ +namespace LearningHub.Nhs.WebUI.Services +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Blazored.LocalStorage; + using Microsoft.Extensions.Logging; + using TELBlazor.Components.Core.Models.Logging; + using TELBlazor.Components.Core.Services.HelperServices; + + /// + /// Provides functionality for managing log levels in applications using NLog. + /// + /// This service implements the interface to provide log + /// level management. Note that NLog does not support runtime log level switching in WASM environments. As a result, + /// methods in this service primarily serve to fulfill the interface contract and provide default + /// behaviors. + public class NLogLogLevelSwitcherService : ILogLevelSwitcherService + { + private const string LogLevelKey = "logLevel"; + private readonly ILogger logger; + private readonly ILocalStorageService localStorage; + + /// + /// Initializes a new instance of the class. + /// + /// The local storage service. + /// The logger instance. + public NLogLogLevelSwitcherService(ILocalStorageService localStorage, ILogger logger) + { + this.localStorage = localStorage; + this.logger = logger; + } + + /// + public bool IsInitialized { get; set; } = false; + + /// + public async Task InitializeLogLevelFromAsyncSourceIfAvailable() + { + // NLog does not support runtime level switching in WASM + // Here only to mirror the interface + this.logger.LogInformation("NLog does not support dynamic runtime log level switching."); + await Task.CompletedTask; + } + + /// + public List GetAvailableLogLevels() => + Enum.GetNames(typeof(LogLevel)).ToList(); + + /// + public string GetCurrentLogLevel() + { + this.logger.LogInformation("Returning default log level (NLog does not support querying runtime level)."); + return "Information"; + } + + /// + public string SetLogLevel(string level) + { + this.logger.LogInformation("Requested to change log level to {Level}, but NLog does not support runtime changes in WASM.", level); + this.LogAllLevels("After 'Change'"); + + _ = this.StoreLogLevelWithTimestamp(level); // Fire and forget + return this.GetCurrentLogLevel(); + } + + private void LogAllLevels(string phase) + { + this.logger.LogTrace("[{Phase}] TRACE log", phase); + this.logger.LogDebug("[{Phase}] DEBUG log", phase); + this.logger.LogInformation("[{Phase}] INFO log", phase); + this.logger.LogWarning("[{Phase}] WARN log", phase); + this.logger.LogError("[{Phase}] ERROR log", phase); + this.logger.LogCritical("[{Phase}] CRITICAL log", phase); + } + + private async Task StoreLogLevelWithTimestamp(string level) + { + try + { + var newItem = new LocalStorageLogLevel + { + Level = level, + Expires = DateTime.UtcNow.AddHours(24), + }; + + await this.localStorage.SetItemAsync(LogLevelKey, newItem); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error storing log level to local storage."); + } + } + } +} diff --git a/LearningHub.Nhs.WebUI/Services/NotificationService.cs b/LearningHub.Nhs.WebUI/Services/NotificationService.cs index e836edcd9..c8c4ce8b9 100644 --- a/LearningHub.Nhs.WebUI/Services/NotificationService.cs +++ b/LearningHub.Nhs.WebUI/Services/NotificationService.cs @@ -9,6 +9,7 @@ using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Notification; using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/OpenApiHttpClient.cs b/LearningHub.Nhs.WebUI/Services/OpenApiHttpClient.cs index 54c3b7bb2..502025b0b 100644 --- a/LearningHub.Nhs.WebUI/Services/OpenApiHttpClient.cs +++ b/LearningHub.Nhs.WebUI/Services/OpenApiHttpClient.cs @@ -2,8 +2,8 @@ { using System.Net.Http; using LearningHub.Nhs.Caching; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; - using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/LearningHub.Nhs.WebUI/Services/PartialFileUploadService.cs b/LearningHub.Nhs.WebUI/Services/PartialFileUploadService.cs index 5ef45d685..c47cbcf80 100644 --- a/LearningHub.Nhs.WebUI/Services/PartialFileUploadService.cs +++ b/LearningHub.Nhs.WebUI/Services/PartialFileUploadService.cs @@ -12,6 +12,7 @@ using Azure.Storage.Files.Shares; using Azure.Storage.Files.Shares.Models; using LearningHub.Nhs.Models.Resource.Files; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.StaticFiles; diff --git a/LearningHub.Nhs.WebUI/Services/ProviderService.cs b/LearningHub.Nhs.WebUI/Services/ProviderService.cs index ee6119c60..7b0ed7d5d 100644 --- a/LearningHub.Nhs.WebUI/Services/ProviderService.cs +++ b/LearningHub.Nhs.WebUI/Services/ProviderService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using LearningHub.Nhs.Caching; using LearningHub.Nhs.Models.Provider; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/RatingService.cs b/LearningHub.Nhs.WebUI/Services/RatingService.cs index fa72f4264..244ae15d2 100644 --- a/LearningHub.Nhs.WebUI/Services/RatingService.cs +++ b/LearningHub.Nhs.WebUI/Services/RatingService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/RegionService.cs b/LearningHub.Nhs.WebUI/Services/RegionService.cs index f05fc22eb..35c56df64 100644 --- a/LearningHub.Nhs.WebUI/Services/RegionService.cs +++ b/LearningHub.Nhs.WebUI/Services/RegionService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/ResourceService.cs b/LearningHub.Nhs.WebUI/Services/ResourceService.cs index ec8eaee30..7a08a1dd0 100644 --- a/LearningHub.Nhs.WebUI/Services/ResourceService.cs +++ b/LearningHub.Nhs.WebUI/Services/ResourceService.cs @@ -15,6 +15,7 @@ namespace LearningHub.Nhs.WebUI.Services using LearningHub.Nhs.Models.Resource.Contribute; using LearningHub.Nhs.Models.Resource.ResourceDisplay; using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models; using Microsoft.Extensions.Logging; diff --git a/LearningHub.Nhs.WebUI/Services/RoadMapService.cs b/LearningHub.Nhs.WebUI/Services/RoadMapService.cs index e63f9b9c1..7759810fd 100644 --- a/LearningHub.Nhs.WebUI/Services/RoadMapService.cs +++ b/LearningHub.Nhs.WebUI/Services/RoadMapService.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; using LearningHub.Nhs.Models.RoadMap; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/RoleService.cs b/LearningHub.Nhs.WebUI/Services/RoleService.cs index c4f37af65..5ce80f927 100644 --- a/LearningHub.Nhs.WebUI/Services/RoleService.cs +++ b/LearningHub.Nhs.WebUI/Services/RoleService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using LearningHub.Nhs.Caching; using LearningHub.Nhs.Models.User; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; diff --git a/LearningHub.Nhs.WebUI/Services/SearchService.cs b/LearningHub.Nhs.WebUI/Services/SearchService.cs index 578a5861f..4c7ca54dd 100644 --- a/LearningHub.Nhs.WebUI/Services/SearchService.cs +++ b/LearningHub.Nhs.WebUI/Services/SearchService.cs @@ -14,6 +14,7 @@ namespace LearningHub.Nhs.WebUI.Services using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Search; using LearningHub.Nhs.Models.Search.SearchClick; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; diff --git a/LearningHub.Nhs.WebUI/Services/SpecialtyService.cs b/LearningHub.Nhs.WebUI/Services/SpecialtyService.cs index 4df5f36e7..dbe7637bf 100644 --- a/LearningHub.Nhs.WebUI/Services/SpecialtyService.cs +++ b/LearningHub.Nhs.WebUI/Services/SpecialtyService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/TermsAndConditionsService.cs b/LearningHub.Nhs.WebUI/Services/TermsAndConditionsService.cs index 6c7220499..dbe6afac3 100644 --- a/LearningHub.Nhs.WebUI/Services/TermsAndConditionsService.cs +++ b/LearningHub.Nhs.WebUI/Services/TermsAndConditionsService.cs @@ -7,6 +7,7 @@ using elfhHub.Nhs.Models.Common; using elfhHub.Nhs.Models.Entities; using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/LearningHub.Nhs.WebUI/Services/UserApiHttpClient.cs b/LearningHub.Nhs.WebUI/Services/UserApiHttpClient.cs index 399ad34cc..8483e44c6 100644 --- a/LearningHub.Nhs.WebUI/Services/UserApiHttpClient.cs +++ b/LearningHub.Nhs.WebUI/Services/UserApiHttpClient.cs @@ -2,8 +2,8 @@ { using System.Net.Http; using LearningHub.Nhs.Caching; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; - using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/LearningHub.Nhs.WebUI/Services/UserGroupService.cs b/LearningHub.Nhs.WebUI/Services/UserGroupService.cs index 22729e5df..3a33a3d60 100644 --- a/LearningHub.Nhs.WebUI/Services/UserGroupService.cs +++ b/LearningHub.Nhs.WebUI/Services/UserGroupService.cs @@ -8,6 +8,7 @@ using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Extensions; using LearningHub.Nhs.Models.User; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; diff --git a/LearningHub.Nhs.WebUI/Services/UserService.cs b/LearningHub.Nhs.WebUI/Services/UserService.cs index 04a2bf68f..3ac652c13 100644 --- a/LearningHub.Nhs.WebUI/Services/UserService.cs +++ b/LearningHub.Nhs.WebUI/Services/UserService.cs @@ -19,6 +19,7 @@ using LearningHub.Nhs.Models.Resource; using LearningHub.Nhs.Models.User; using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.Shared.Interfaces.Http; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs index 0a0c04c49..6ea200c45 100644 --- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs +++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs @@ -1,19 +1,26 @@ namespace LearningHub.Nhs.WebUI.Startup { + using System; using System.Net.Http; + using Blazored.LocalStorage; using GDS.MultiPageFormData; using LearningHub.Nhs.Models.OpenAthens; using LearningHub.Nhs.Services; using LearningHub.Nhs.Services.Interface; + using LearningHub.Nhs.Shared.Interfaces.Http; + using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Filters; using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.JsDetection; using LearningHub.Nhs.WebUI.Services; using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using TELBlazor.Components.Core.Configuration; + using TELBlazor.Components.Core.Services.HelperServices; /// /// The service mappings. @@ -81,6 +88,13 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon // Config services.Configure(configuration.GetSection("OpenAthensScopes")); + services.Configure(configuration.GetSection("Settings:" + BFFPathValidationOptions.SectionName)); + + // Blazor + services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddCircuitOptions(opt => opt.DetailedErrors = true) + .AddInteractiveWebAssemblyComponents(); // Learning Hub Services services.AddTransient(); @@ -127,6 +141,34 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // + // Future candidates for DI collection + services.AddBlazoredLocalStorage(); + + /* The base TELBlazor Configuration inherited by other components uses this configuration to tell blazor components ahead of time if the browser has Javascript (need to load the wasm and hydrate) via JsEnabled. + This allows for logic and UI to be implemented specifically for no js if desired without a second load of the component, where this may be desireable. + Host information is also provided which is useful for debugging. + */ + services.AddSingleton(provider => + { + var httpContextAccessor = provider.GetRequiredService(); + var context = httpContextAccessor.HttpContext; + bool jsEnabled = false; + + if (context != null && context.Request.Cookies.TryGetValue("jsEnabled", out var jsCookieValue)) + { + jsEnabled = jsCookieValue == "true"; + } + + return new TELBlazorBaseComponentConfiguration + { + JSEnabled = jsEnabled, + HostType = $"{configuration["Properties:Environment"]} {configuration["Properties:Application"]}", + }; + }); + + services.AddScoped(); } } } diff --git a/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml b/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml index 3a681c901..3fcd1639c 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/LandingPage.cshtml @@ -37,7 +37,6 @@ Sign up, explore and learn -
diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml index 6b679b67e..c1dccf585 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml @@ -51,11 +51,13 @@ @await Html.PartialAsync("_GoogleAnalytics") + Skip to main content @@ -159,6 +161,7 @@ + @RenderSection("Scripts", required: false) diff --git a/global.json b/global.json new file mode 100644 index 000000000..0974123a3 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "8.0.412" + } +} \ No newline at end of file diff --git a/nuget.config.cicd b/nuget.config.cicd new file mode 100644 index 000000000..bb1f051c9 --- /dev/null +++ b/nuget.config.cicd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nuget.config.template b/nuget.config.template new file mode 100644 index 000000000..ec515b2d1 --- /dev/null +++ b/nuget.config.template @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +