From 4221ca1f2c1efb33ff8c2dfc4be8f09ea72af072 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sun, 29 Mar 2026 13:26:51 +0100 Subject: [PATCH 01/18] Checkpoint from VS Code for cloud agent session --- docker-compose.yaml | 13 ++++ ...fE.CheckPerformanceData.Application.csproj | 9 +++ .../DfESignInApiClient/IDfESignInApiClient.cs | 6 ++ .../DfESignInApiClient/OrganisationDto.cs | 67 +++++++++++++++++++ ...CheckPerformanceData.Infrastructure.csproj | 24 +++++++ .../DfeSignIn/DfeSignInAuthExtensions.cs | 65 ++++++++++++++++++ .../DfeSignInApiClient/DfeSignInApiClient.cs | 21 ++++++ .../DfeSignInApiClientExtensions.cs | 42 ++++++++++++ .../DfeSignInApiClient/DfeSigninSettings.cs | 12 ++++ .../Controllers/HomeController.cs | 2 + .../Controllers/SecretController.cs | 37 ++++++++++ .../Controllers/SecretViewModel.cs | 8 +++ .../DfE.CheckPerformanceData.Web.csproj | 3 + src/DfE.CheckPerformanceData.Web/Program.cs | 14 +++- .../Views/Home/Index.cshtml | 10 +-- .../Views/Secret/Index.cshtml | 9 +++ src/DfE.CheckPerformanceData.slnx | 2 + src/Directory.Packages.props | 9 +++ 18 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj create mode 100644 src/DfE.CheckPerformanceData.Application/DfESignInApiClient/IDfESignInApiClient.cs create mode 100644 src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClientExtensions.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSigninSettings.cs create mode 100644 src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs create mode 100644 src/DfE.CheckPerformanceData.Web/Controllers/SecretViewModel.cs create mode 100644 src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml diff --git a/docker-compose.yaml b/docker-compose.yaml index d945f42..ae1b553 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,6 +12,13 @@ - ASPNETCORE_URLS=http://+:8080 - ConnectionStrings__Postgres=${POSTGRES_CONNECTION_STRING:-Host=db;Database=helloworld;Username=postgres;Password=postgres} - ConnectionStrings__AzureStorage=${AZURE_STORAGE_CONNECTION_STRING:-DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://azurite:10001/devstoreaccount1;} + - DfeSignIn__BaseUrl=${DFESIGNIN_BASEURL:-https://test-api.signin.education.gov.uk/} + - DfeSignIn__ClientId=${DFESIGNIN_CLIENTID} + - DfeSignIn__ApiClientSecret=${DFESIGNIN_APICLIENTSECRET} + - DfeSignIn__Audience=${DFESIGNIN_AUDIENCE:-signin.education.gov.uk} + - DfeSignIn__MetadataAddress=${DFESIGNIN_METADATAADDRESS:-https://test-oidc.signin.education.gov.uk/.well-known/openid-configuration} + - DfeSignIn__ClientSecret=${DFESIGNIN_CLIENTSECRET} + depends_on: - db - azurite @@ -25,6 +32,12 @@ environment: - ConnectionStrings__Postgres=${POSTGRES_CONNECTION_STRING:-Host=db;Database=helloworld;Username=postgres;Password=postgres} - ConnectionStrings__AzureStorage=${AZURE_STORAGE_CONNECTION_STRING:-DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://azurite:10001/devstoreaccount1;} + - DfeSignIn__BaseUrl=https://test-api.signin.education.gov.uk/ + - DfeSignIn__ClientId=CheckPerformanceData + - DfeSignIn__ApiClientSecret=kineplasty-sickened-camomile-sestine + - DfeSignIn__Audience=signin.education.gov.uk + - DfeSignIn__MetadataAddress=https://test-oidc.signin.education.gov.uk/.well-known/openid-configuration + - DfeSignIn__ClientSecret=inelegant-provide-joining-atheism - RulesEngineOptions__QueueName=${RULESENGINEOPTIONS_QUEUENAME:-requests} - RulesEngineOptions__RetryDelayMs=${RULESENGINEOPTIONS_RETRYDELAYMS:-1000} - RulesEngineOptions__MaxMessagesPerPoll=${RULESENGINEOPTIONS_MAXMESSAGESPERPOLL:-10} diff --git a/src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj b/src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj new file mode 100644 index 0000000..237d661 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/IDfESignInApiClient.cs b/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/IDfESignInApiClient.cs new file mode 100644 index 0000000..cbb7d9f --- /dev/null +++ b/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/IDfESignInApiClient.cs @@ -0,0 +1,6 @@ +namespace DfE.CheckPerformanceData.Application.DfESignInApiClient; + +public interface IDfESignInApiClient +{ + Task GetOrganisationAsync(string userId, string organisationId); +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs b/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs new file mode 100644 index 0000000..9949d57 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs @@ -0,0 +1,67 @@ +namespace DfE.CheckPerformanceData.Application.DfESignInApiClient; + +public class OrganisationDto +{ + public string Id { get; init; } + public string Name { get; init; } = string.Empty; + public string? LegalName { get; init; } + + public string? Urn { get; init; } + public string? Uid { get; init; } + public string? Upin { get; init; } + public string? Ukprn { get; init; } + public string? EstablishmentNumber { get; init; } + + public string? Address { get; init; } + public string? Telephone { get; init; } + public int? StatutoryLowAge { get; init; } + public int? StatutoryHighAge { get; init; } + public string? LegacyId { get; init; } + public string? CompanyRegistrationNumber { get; init; } +} + + +// +// [ { +// "id" : "09158CF5-A701-47E8-BDCD-4EA201B024A3", +// "name" : "Department for Education", +// "LegalName" : null, +// "category" : { +// "id" : "002", +// "name" : "Local Authority" +// }, +// "urn" : null, +// "uid" : null, +// "upin" : null, +// "ukprn" : null, +// "establishmentNumber" : "001", +// "status" : { +// "id" : 1, +// "name" : "Open", +// "tagColor" : "green" +// }, +// "closedOn" : null, +// "address" : null, +// "telephone" : null, +// "statutoryLowAge" : null, +// "statutoryHighAge" : null, +// "legacyId" : "1031237", +// "companyRegistrationNumber" : null, +// "SourceSystem" : null, +// "providerTypeName" : null, +// "ProviderTypeCode" : null, +// "GIASProviderType" : null, +// "PIMSProviderType" : null, +// "PIMSProviderTypeCode" : null, +// "PIMSStatusName" : null, +// "pimsStatus" : null, +// "GIASStatusName" : null, +// "GIASStatus" : null, +// "MasterProviderStatusName" : null, +// "MasterProviderStatusCode" : null, +// "OpenedOn" : null, +// "DistrictAdministrativeName" : null, +// "DistrictAdministrativeCode" : null, +// "DistrictAdministrative_code" : null, +// "IsOnAPAR" : null +// } ] diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj b/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj new file mode 100644 index 0000000..6147cf4 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs new file mode 100644 index 0000000..cad4519 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs @@ -0,0 +1,65 @@ +using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace DfE.CheckPerformanceData.Infrastructure.DfeSignIn; + +public static class DfeSignInAuthExtensions +{ + public static IServiceCollection AddDfeSignInAuthentication(this IServiceCollection services, IConfiguration config) + { + var settings = config.GetSection(DfeSigninSettings.SectionName).Get(); + + services.AddAuthentication(options => + { + options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(30); + o.SlidingExpiration = true; + // o.LogoutPath = "/auth/logout"; + // + // o.Events.OnRedirectToAccessDenied = ctx => + // { + // ctx.Response.StatusCode = 403; + // ctx.Response.Redirect("/user-with-no-role"); + // return Task.CompletedTask; + // }; + }).AddOpenIdConnect(options => + { + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.MetadataAddress = settings!.MetadataAddress; + options.ClientId = settings.ClientId; + options.ClientSecret = settings.ClientSecret; + options.ResponseType = OpenIdConnectResponseType.Code; + options.RequireHttpsMetadata = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; + options.CallbackPath = "/auth/callback"; + options.SignedOutCallbackPath = "/auth/signout-callback"; + + options.Scope.Clear(); + options.Scope.Add("email"); + options.Scope.Add("sub"); + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("organisationid"); + + options.Events.OnTokenResponseReceived = ctx => + { + //ctx.HttpContext.Response.Headers.Add("Cache-Control", "no-store"); + return Task.CompletedTask; + }; + }); + + return services; + } +} + + diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs new file mode 100644 index 0000000..29220ea --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs @@ -0,0 +1,21 @@ +using System.Net.Http.Json; +using DfE.CheckPerformanceData.Application.DfESignInApiClient; + +namespace DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; + +public class DfeSignInApiClient : IDfESignInApiClient +{ + private readonly HttpClient _httpClient; + + public DfeSignInApiClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task GetOrganisationAsync(string userId, string organisationId) + { + var userOrganisations = await _httpClient.GetFromJsonAsync>($"users/{userId}/organisations"); + + return userOrganisations?.FirstOrDefault(o => o.Id == organisationId); + } +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClientExtensions.cs b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClientExtensions.cs new file mode 100644 index 0000000..802ee72 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClientExtensions.cs @@ -0,0 +1,42 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http.Headers; +using System.Text; +using DfE.CheckPerformanceData.Application.DfESignInApiClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; + +public static class DfeSignInApiClientExtensions +{ + public static IServiceCollection AddDfeApiClient(this IServiceCollection services, IConfiguration config) + { + services.Configure(config.GetSection(DfeSigninSettings.SectionName)); + + services.AddHttpClient((serviceProvider, client) => + { + var settings = serviceProvider.GetRequiredService>().Value; + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.ApiClientSecret)); + var descriptor = new SecurityTokenDescriptor() + { + Issuer = settings.ClientId, + Audience = settings.Audience, + Expires = DateTime.UtcNow.AddMinutes(5), + SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateEncodedJwt(descriptor); + + client.BaseAddress = new Uri(settings.BaseUrl); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + }); + + return services; + } +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSigninSettings.cs b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSigninSettings.cs new file mode 100644 index 0000000..c3af133 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSigninSettings.cs @@ -0,0 +1,12 @@ +namespace DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; + +public class DfeSigninSettings +{ + public const string SectionName = "DfeSignIn"; + public string BaseUrl { get; set; } = string.Empty; + public string ApiClientSecret { get; set; } + public string ClientId { get; set; } + public string Audience { get; set; } + public string? MetadataAddress { get; set; } + public string? ClientSecret { get; set; } +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs b/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs index f379bce..686a4c5 100644 --- a/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs +++ b/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs @@ -20,6 +20,8 @@ public async Task SendRequest() { var client = queueServiceClient.GetQueueClient("requests"); + await client.CreateIfNotExistsAsync(); + await client.SendMessageAsync($"Hello, World! Time is {DateTime.Now.ToShortTimeString()}"); // Handle the POST here diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs new file mode 100644 index 0000000..838a540 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs @@ -0,0 +1,37 @@ +using System.Security.Claims; +using System.Text.Json.Nodes; +using DfE.CheckPerformanceData.Application.DfESignInApiClient; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DfE.CheckPerformanceData.Web.Controllers; + +public class SecretController : Controller +{ + private readonly IDfESignInApiClient _dfeSignInApiClient; + + public SecretController(IDfESignInApiClient dfeSignInApiClient) + { + _dfeSignInApiClient = dfeSignInApiClient; + } + + [Authorize] + public async Task Index() + { + var userid = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + var orgJson = User.FindFirst("organisation")?.Value ?? "{}"; + var org = JsonNode.Parse(orgJson); + var orgId = org["id"]?.ToString() ?? string.Empty; + + var organisation = await _dfeSignInApiClient.GetOrganisationAsync(userid, orgId); + + var vm = new SecretViewModel() + { + UserName = User.FindFirstValue(ClaimTypes.GivenName) + " " + User.FindFirstValue(ClaimTypes.Surname), + OrganisationName = organisation?.Name + }; + + return View(vm); + } +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/SecretViewModel.cs b/src/DfE.CheckPerformanceData.Web/Controllers/SecretViewModel.cs new file mode 100644 index 0000000..cb54988 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Web/Controllers/SecretViewModel.cs @@ -0,0 +1,8 @@ +namespace DfE.CheckPerformanceData.Web.Controllers; + +public class SecretViewModel +{ + public string UserName { get; set; } + public string OrganisationName { get; set; } + +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj b/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj index 5471c5f..b35e650 100644 --- a/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj +++ b/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj @@ -12,9 +12,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index be85256..ec7447d 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -1,12 +1,19 @@ using Azure.Storage.Queues; using DfE.CheckPerformanceData.Data; +using DfE.CheckPerformanceData.Infrastructure.DfeSignIn; +using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; using GovUk.Frontend.AspNetCore; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -builder.Services.AddControllersWithViews(); +builder.Services + .AddDfeApiClient(builder.Configuration) + .AddDfeSignInAuthentication(builder.Configuration) + .AddGovUkFrontend(options => options.Rebrand = true); builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Postgres"))); @@ -17,7 +24,7 @@ MessageEncoding = QueueMessageEncoding.Base64 })); -builder.Services.AddGovUkFrontend(options => options.Rebrand = true); +builder.Services.AddControllersWithViews(); builder.Services.AddHealthChecks(); @@ -38,6 +45,7 @@ app.UseHttpsRedirection(); app.UseRouting(); +app.UseAuthentication(); app.UseAuthorization(); app.MapStaticAssets(); diff --git a/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml b/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml index d0543f1..5be8961 100644 --- a/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml +++ b/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml @@ -9,11 +9,13 @@

Two-thirds column

-
- @Html.AntiForgeryToken() + + @Html.AntiForgeryToken() - Send Test Message -
+ Send Test Message + + + Secret page
diff --git a/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml new file mode 100644 index 0000000..1f76fce --- /dev/null +++ b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml @@ -0,0 +1,9 @@ +@using System.Security.Claims +@model DfE.CheckPerformanceData.Web.Controllers.SecretViewModel; + +@{ + ViewBag.Title = "title"; + Layout = "_Layout"; +} + +

Secret page. Hello @Model.UserName of @Model.OrganisationName

diff --git a/src/DfE.CheckPerformanceData.slnx b/src/DfE.CheckPerformanceData.slnx index 080a2d4..e29f607 100644 --- a/src/DfE.CheckPerformanceData.slnx +++ b/src/DfE.CheckPerformanceData.slnx @@ -5,7 +5,9 @@ + + \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 264b9fb..1b7f97f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,8 +6,16 @@ + + + + + + + + @@ -15,5 +23,6 @@ + \ No newline at end of file From 4aa5f8bf6f701ba15a1bcc4b702832305967be2f Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sun, 29 Mar 2026 13:30:57 +0100 Subject: [PATCH 02/18] Using tidy --- src/DfE.CheckPerformanceData.Web/Program.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index ec7447d..1ee2297 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -3,10 +3,7 @@ using DfE.CheckPerformanceData.Infrastructure.DfeSignIn; using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; using GovUk.Frontend.AspNetCore; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; var builder = WebApplication.CreateBuilder(args); From 4ac19353e2950cb0659b818964020bcd7a3ef871 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sun, 29 Mar 2026 14:25:11 +0100 Subject: [PATCH 03/18] Refactor of persistence --- .../DfE.CheckPerformanceData.Application.csproj | 8 ++++++++ .../IPortalDbContext.cs | 10 ++++++++++ .../DfE.CheckPerformanceData.Data.csproj | 12 ------------ src/DfE.CheckPerformanceData.Data/PortalDbContext.cs | 9 --------- .../DfE.CheckPerformanceData.Domain.csproj | 9 +++++++++ .../Entities/Workflow.cs | 2 +- .../DfE.CheckPerformanceData.Infrastructure.csproj | 7 +++++++ .../Persistence/PortalDbContext.cs | 10 ++++++++++ .../Controllers/HomeController.cs | 5 ++--- .../Controllers/SecretController.cs | 1 + .../Controllers/{ => ViewModels}/SecretViewModel.cs | 2 +- .../DfE.CheckPerformanceData.Web.csproj | 2 -- src/DfE.CheckPerformanceData.Web/Program.cs | 5 ++++- .../Views/Secret/Index.cshtml | 2 +- src/DfE.CheckPerformanceData.slnx | 2 +- src/Directory.Packages.props | 1 + 16 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs delete mode 100644 src/DfE.CheckPerformanceData.Data/DfE.CheckPerformanceData.Data.csproj delete mode 100644 src/DfE.CheckPerformanceData.Data/PortalDbContext.cs create mode 100644 src/DfE.CheckPerformanceData.Domain/DfE.CheckPerformanceData.Domain.csproj rename src/{DfE.CheckPerformanceData.Data => DfE.CheckPerformanceData.Domain}/Entities/Workflow.cs (64%) create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs rename src/DfE.CheckPerformanceData.Web/Controllers/{ => ViewModels}/SecretViewModel.cs (67%) diff --git a/src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj b/src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj index 237d661..65f0f97 100644 --- a/src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj +++ b/src/DfE.CheckPerformanceData.Application/DfE.CheckPerformanceData.Application.csproj @@ -6,4 +6,12 @@ enable + + + + + + + + diff --git a/src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs b/src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs new file mode 100644 index 0000000..e2cc0b0 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs @@ -0,0 +1,10 @@ +using DfE.CheckPerformanceData.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace DfE.CheckPerformanceData.Application; + +public interface IPortalDbContext +{ + DbSet Workflows { get; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Data/DfE.CheckPerformanceData.Data.csproj b/src/DfE.CheckPerformanceData.Data/DfE.CheckPerformanceData.Data.csproj deleted file mode 100644 index c85341b..0000000 --- a/src/DfE.CheckPerformanceData.Data/DfE.CheckPerformanceData.Data.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net10.0 - enable - enable - - - - - - \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Data/PortalDbContext.cs b/src/DfE.CheckPerformanceData.Data/PortalDbContext.cs deleted file mode 100644 index 425fb02..0000000 --- a/src/DfE.CheckPerformanceData.Data/PortalDbContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -using DfE.CheckPerformanceData.Data.Entities; -using Microsoft.EntityFrameworkCore; - -namespace DfE.CheckPerformanceData.Data; - -public class PortalDbContext(DbContextOptions options) : DbContext(options) -{ - public DbSet Workflows => Set(); -} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Domain/DfE.CheckPerformanceData.Domain.csproj b/src/DfE.CheckPerformanceData.Domain/DfE.CheckPerformanceData.Domain.csproj new file mode 100644 index 0000000..237d661 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Domain/DfE.CheckPerformanceData.Domain.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/src/DfE.CheckPerformanceData.Data/Entities/Workflow.cs b/src/DfE.CheckPerformanceData.Domain/Entities/Workflow.cs similarity index 64% rename from src/DfE.CheckPerformanceData.Data/Entities/Workflow.cs rename to src/DfE.CheckPerformanceData.Domain/Entities/Workflow.cs index c0914bd..f4b4807 100644 --- a/src/DfE.CheckPerformanceData.Data/Entities/Workflow.cs +++ b/src/DfE.CheckPerformanceData.Domain/Entities/Workflow.cs @@ -1,5 +1,5 @@ -namespace DfE.CheckPerformanceData.Data.Entities; +namespace DfE.CheckPerformanceData.Domain.Entities; public class Workflow { diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj b/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj index 6147cf4..594a606 100644 --- a/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj @@ -8,17 +8,24 @@ + + + + + + + diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs b/src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs new file mode 100644 index 0000000..aef69fe --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs @@ -0,0 +1,10 @@ +using DfE.CheckPerformanceData.Application; +using DfE.CheckPerformanceData.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace DfE.CheckPerformanceData.Infrastructure.Persistence; + +public class PortalDbContext(DbContextOptions options) : DbContext(options), IPortalDbContext +{ + public DbSet Workflows => Set(); +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs b/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs index 686a4c5..794b9cd 100644 --- a/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs +++ b/src/DfE.CheckPerformanceData.Web/Controllers/HomeController.cs @@ -1,13 +1,12 @@ using System.Diagnostics; using Azure.Storage.Queues; -using DfE.CheckPerformanceData.Data; -using DfE.CheckPerformanceData.Data.Entities; +using DfE.CheckPerformanceData.Application; using Microsoft.AspNetCore.Mvc; using DfE.CheckPerformanceData.Web.Models; namespace DfE.CheckPerformanceData.Web.Controllers; -public class HomeController(PortalDbContext context, QueueServiceClient queueServiceClient) : Controller +public class HomeController(IPortalDbContext context, QueueServiceClient queueServiceClient) : Controller { public async Task Index() { diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs index 838a540..2abc1ab 100644 --- a/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs +++ b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Text.Json.Nodes; using DfE.CheckPerformanceData.Application.DfESignInApiClient; +using DfE.CheckPerformanceData.Web.Controllers.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/SecretViewModel.cs b/src/DfE.CheckPerformanceData.Web/Controllers/ViewModels/SecretViewModel.cs similarity index 67% rename from src/DfE.CheckPerformanceData.Web/Controllers/SecretViewModel.cs rename to src/DfE.CheckPerformanceData.Web/Controllers/ViewModels/SecretViewModel.cs index cb54988..d80f816 100644 --- a/src/DfE.CheckPerformanceData.Web/Controllers/SecretViewModel.cs +++ b/src/DfE.CheckPerformanceData.Web/Controllers/ViewModels/SecretViewModel.cs @@ -1,4 +1,4 @@ -namespace DfE.CheckPerformanceData.Web.Controllers; +namespace DfE.CheckPerformanceData.Web.Controllers.ViewModels; public class SecretViewModel { diff --git a/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj b/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj index b35e650..934f1ad 100644 --- a/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj +++ b/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj @@ -16,8 +16,6 @@ - \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index 1ee2297..4153438 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -1,7 +1,8 @@ using Azure.Storage.Queues; -using DfE.CheckPerformanceData.Data; +using DfE.CheckPerformanceData.Application; using DfE.CheckPerformanceData.Infrastructure.DfeSignIn; using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; +using DfE.CheckPerformanceData.Infrastructure.Persistence; using GovUk.Frontend.AspNetCore; using Microsoft.EntityFrameworkCore; @@ -15,6 +16,8 @@ builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Postgres"))); +builder.Services.AddScoped(sp => sp.GetRequiredService()); + builder.Services.AddSingleton(_ => new QueueServiceClient(builder.Configuration.GetConnectionString("AzureStorage"), new QueueClientOptions(QueueClientOptions.ServiceVersion.V2025_11_05) { diff --git a/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml index 1f76fce..f1eb74e 100644 --- a/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml +++ b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml @@ -1,5 +1,5 @@ @using System.Security.Claims -@model DfE.CheckPerformanceData.Web.Controllers.SecretViewModel; +@model DfE.CheckPerformanceData.Web.Controllers.ViewModels.SecretViewModel; @{ ViewBag.Title = "title"; diff --git a/src/DfE.CheckPerformanceData.slnx b/src/DfE.CheckPerformanceData.slnx index e29f607..3bbdbe3 100644 --- a/src/DfE.CheckPerformanceData.slnx +++ b/src/DfE.CheckPerformanceData.slnx @@ -6,7 +6,7 @@ - + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 1b7f97f..63f93c9 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -9,6 +9,7 @@ + From 8b5e6c0fa117f8c20ebc395083a6edc47023c69a Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sun, 29 Mar 2026 14:42:56 +0100 Subject: [PATCH 04/18] Updated readme --- README.md | 6 +++++- src/DfE.CheckPerformanceData.slnx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ed06772..af1ebd1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Prerequisites - [Git](https://git-scm.com/downloads) (for getting a copy of the source code and contributing changes) - [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) (for building and running the C#/.NET web application) -- [Node.js](https://nodejs.org/en/download/) (for building web artefacts: (S)CSS, JS, etc.) +- ~~[Node.js](https://nodejs.org/en/download/) (for building web artefacts: (S)CSS, JS, etc.)~~ - IDE/Editor of choice (e.g., Visual Studio, Visual Studio Code, JetBrains Rider, etc.) - [Docker Desktop](https://docs.docker.com/desktop/) (for development time hosting of dependencies) @@ -25,4 +25,8 @@ dotnet build Confirm tests are passing locally ```sh dotnet test +``` + +```sh +docker compose up --build -d ``` \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.slnx b/src/DfE.CheckPerformanceData.slnx index 3bbdbe3..dfe5228 100644 --- a/src/DfE.CheckPerformanceData.slnx +++ b/src/DfE.CheckPerformanceData.slnx @@ -3,6 +3,7 @@ + From 7df5ddd04f9735ec986ab48c0df6964a344c4593 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Tue, 31 Mar 2026 08:18:57 +0100 Subject: [PATCH 05/18] Secret display --- .../Controllers/SecretController.cs | 19 ++- .../Controllers/ViewModels/SecretViewModel.cs | 6 +- .../Views/Home/Index.cshtml | 53 +++++++-- .../Views/Secret/Index.cshtml | 83 +++++++++++++- .../Views/Shared/_Layout.cshtml | 108 +++++++++++++----- 5 files changed, 223 insertions(+), 46 deletions(-) diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs index 2abc1ab..ac478f1 100644 --- a/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs +++ b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs @@ -2,6 +2,9 @@ using System.Text.Json.Nodes; using DfE.CheckPerformanceData.Application.DfESignInApiClient; using DfE.CheckPerformanceData.Web.Controllers.ViewModels; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -30,9 +33,23 @@ public async Task Index() var vm = new SecretViewModel() { UserName = User.FindFirstValue(ClaimTypes.GivenName) + " " + User.FindFirstValue(ClaimTypes.Surname), - OrganisationName = organisation?.Name + Organisation = organisation }; return View(vm); } + + public async Task SignOut() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + return SignOut( + new AuthenticationProperties + { + RedirectUri = Url.Action("Index", "Home") + }, + CookieAuthenticationDefaults.AuthenticationScheme, + OpenIdConnectDefaults.AuthenticationScheme + ); + } } \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/ViewModels/SecretViewModel.cs b/src/DfE.CheckPerformanceData.Web/Controllers/ViewModels/SecretViewModel.cs index d80f816..c5bc914 100644 --- a/src/DfE.CheckPerformanceData.Web/Controllers/ViewModels/SecretViewModel.cs +++ b/src/DfE.CheckPerformanceData.Web/Controllers/ViewModels/SecretViewModel.cs @@ -1,8 +1,10 @@ +using DfE.CheckPerformanceData.Application.DfESignInApiClient; + namespace DfE.CheckPerformanceData.Web.Controllers.ViewModels; public class SecretViewModel { public string UserName { get; set; } - public string OrganisationName { get; set; } - + //public string OrganisationName { get; set; } + public OrganisationDto Organisation { get; set; } } \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml b/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml index 5be8961..199b8dc 100644 --- a/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml +++ b/src/DfE.CheckPerformanceData.Web/Views/Home/Index.cshtml @@ -2,26 +2,55 @@ ViewData["Title"] = "Home Page"; } -
- Back -
-

Two-thirds column

+

Check your performance measures data

-
+

+ Review and confirm the pupil data that will be used to calculate your school or college's performance measures. +

+ +

+ Use this service to: +

+
    +
  • check which pupils are included in your performance data
  • +
  • request changes if a pupil should not be counted
  • +
  • upload evidence to support removal requests
  • +
  • review summary information before it is finalised
  • +
+

+ This helps make sure your school's published performance data is accurate and up to date. +

+

+ You can use this service if you are: +

+
    +
  • a school or college with pupils included in performance measures
  • +
  • responsible for checking or submitting data (for example, headteacher or data manager)
  • +
+ + @* @Html.AntiForgeryToken() Send Test Message -
- - Secret page + *@ + + + Start now + + +

+ Sign up to get email notifications about this service. +

-

One-third column

-

+

Guidance

+

+ How to check your performance data +

-
-
diff --git a/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml index f1eb74e..8bc87b6 100644 --- a/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml +++ b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml @@ -1,9 +1,86 @@ -@using System.Security.Claims -@model DfE.CheckPerformanceData.Web.Controllers.ViewModels.SecretViewModel; +@model DfE.CheckPerformanceData.Web.Controllers.ViewModels.SecretViewModel; @{ ViewBag.Title = "title"; Layout = "_Layout"; } -

Secret page. Hello @Model.UserName of @Model.OrganisationName

+

Hello @Model.UserName of @Model.Organisation.Name

+ +
+ +
+
ID
+
@Model.Organisation.Id
+
+ +
+
Name
+
@Model.Organisation.Name
+
+ +
+
Legal name
+
@(Model.Organisation.LegalName ?? "Not provided")
+
+ +
+
URN
+
@(Model.Organisation.Urn ?? "Not provided")
+
+ +
+
UID
+
@(Model.Organisation.Uid ?? "Not provided")
+
+ +
+
UPIN
+
@(Model.Organisation.Upin ?? "Not provided")
+
+ +
+
UKPRN
+
@(Model.Organisation.Ukprn ?? "Not provided")
+
+ +
+
Establishment number
+
@(Model.Organisation.EstablishmentNumber ?? "Not provided")
+
+ +
+
Address
+
@(Model.Organisation.Address ?? "Not provided")
+
+ +
+
Telephone
+
@(Model.Organisation.Telephone ?? "Not provided")
+
+ +
+
Statutory age range
+
+ @if (Model.Organisation.StatutoryLowAge.HasValue && Model.Organisation.StatutoryHighAge.HasValue) + { + @($"{Model.Organisation.StatutoryLowAge} to {Model.Organisation.StatutoryHighAge}") + } + else + { + Not provided + } +
+
+ +
+
Legacy ID
+
@(Model.Organisation.LegacyId ?? "Not provided")
+
+ +
+
Company registration number
+
@(Model.Organisation.CompanyRegistrationNumber ?? "Not provided")
+
+ +
diff --git a/src/DfE.CheckPerformanceData.Web/Views/Shared/_Layout.cshtml b/src/DfE.CheckPerformanceData.Web/Views/Shared/_Layout.cshtml index 4b87524..1253eee 100644 --- a/src/DfE.CheckPerformanceData.Web/Views/Shared/_Layout.cshtml +++ b/src/DfE.CheckPerformanceData.Web/Views/Shared/_Layout.cshtml @@ -20,39 +20,91 @@ } @section Header { - - + - @* *@ - @* Navigation item 1 *@ - @* Navigation item 2 *@ - @* Navigation item 3 *@ - @* *@ + + @if (@User.Identity is { IsAuthenticated: true }) + { + Sign out + + } + +
+

+ + + This is a new service – your feedback will help us to improve it. + +

+
} @RenderBody() @section Footer { - - - Two column list - - Navigation item 1 - Navigation item 2 - Navigation item 3 - Navigation item 4 - Navigation item 5 - Navigation item 6 - - - - Single column list - - Navigation item 1 - Navigation item 2 - Navigation item 3 - - - + } \ No newline at end of file From c5fcb690a03261e05df547d1b4ad16f207bba046 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Tue, 31 Mar 2026 08:38:14 +0100 Subject: [PATCH 06/18] Added serilog --- .../DfE.CheckPerformanceData.Web.csproj | 4 + src/DfE.CheckPerformanceData.Web/Program.cs | 113 ++++++++++++------ .../appsettings.json | 16 ++- src/Directory.Packages.props | 4 + 4 files changed, 95 insertions(+), 42 deletions(-) diff --git a/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj b/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj index 934f1ad..8faf55b 100644 --- a/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj +++ b/src/DfE.CheckPerformanceData.Web/DfE.CheckPerformanceData.Web.csproj @@ -13,6 +13,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index 4153438..c3c7bc4 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -5,55 +5,90 @@ using DfE.CheckPerformanceData.Infrastructure.Persistence; using GovUk.Frontend.AspNetCore; using Microsoft.EntityFrameworkCore; +using Serilog; +using Serilog.Formatting.Compact; +using Serilog.Templates; +using Serilog.Templates.Themes; -var builder = WebApplication.CreateBuilder(args); +Log.Logger = new LoggerConfiguration() + .WriteTo.Console(new CompactJsonFormatter()) + .CreateBootstrapLogger(); -builder.Services - .AddDfeApiClient(builder.Configuration) - .AddDfeSignInAuthentication(builder.Configuration) - .AddGovUkFrontend(options => options.Rebrand = true); - -builder.Services.AddDbContext(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("Postgres"))); +try +{ + Log.Information("Starting application"); -builder.Services.AddScoped(sp => sp.GetRequiredService()); + var builder = WebApplication.CreateBuilder(args); -builder.Services.AddSingleton(_ => new QueueServiceClient(builder.Configuration.GetConnectionString("AzureStorage"), - new QueueClientOptions(QueueClientOptions.ServiceVersion.V2025_11_05) + builder.Host.UseSerilog((context, services, configuration) => { - MessageEncoding = QueueMessageEncoding.Base64 - })); - -builder.Services.AddControllersWithViews(); - -builder.Services.AddHealthChecks(); - -var app = builder.Build(); - -app.UseGovUkFrontend(); - -app.UseHealthChecks("/healthcheck"); + var isDevelopment = context.HostingEnvironment.IsDevelopment(); + + configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console(isDevelopment + ? new ExpressionTemplate( + "[{@t:HH:mm:ss} {@l:u3}] {SourceContext}\n {@m}\n{@x}", + theme: TemplateTheme.Code) + : new CompactJsonFormatter()); + }); + + builder.Services + .AddDfeApiClient(builder.Configuration) + .AddDfeSignInAuthentication(builder.Configuration) + .AddGovUkFrontend(options => options.Rebrand = true); + + builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("Postgres"))); + + builder.Services.AddScoped(sp => sp.GetRequiredService()); + + builder.Services.AddSingleton(_ => new QueueServiceClient(builder.Configuration.GetConnectionString("AzureStorage"), + new QueueClientOptions(QueueClientOptions.ServiceVersion.V2025_11_05) + { + MessageEncoding = QueueMessageEncoding.Base64 + })); + + builder.Services.AddControllersWithViews(); + + builder.Services.AddHealthChecks(); + + var app = builder.Build(); + + app.UseSerilogRequestLogging(); + + app.UseGovUkFrontend(); + + app.UseHealthChecks("/healthcheck"); // Configure the HTTP request pipeline. -if (!app.Environment.IsDevelopment()) -{ - app.UseExceptionHandler("/Home/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} + if (!app.Environment.IsDevelopment()) + { + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } -app.UseHttpsRedirection(); -app.UseRouting(); + app.UseHttpsRedirection(); + app.UseRouting(); -app.UseAuthentication(); -app.UseAuthorization(); + app.UseAuthentication(); + app.UseAuthorization(); -app.MapStaticAssets(); + app.MapStaticAssets(); -app.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}") - .WithStaticAssets(); + app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithStaticAssets(); -app.Run(); + app.Run(); +} +catch (Exception e) +{ + Console.WriteLine(e); + throw; +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/appsettings.json b/src/DfE.CheckPerformanceData.Web/appsettings.json index 10f68b8..463f06b 100644 --- a/src/DfE.CheckPerformanceData.Web/appsettings.json +++ b/src/DfE.CheckPerformanceData.Web/appsettings.json @@ -1,9 +1,19 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "System": "Warning" + } } }, +// "Logging": { +// "LogLevel": { +// "Default": "Information", +// "Microsoft.AspNetCore": "Warning" +// } +// }, "AllowedHosts": "*" } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 63f93c9..560f80f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -24,6 +24,10 @@ + + + +
\ No newline at end of file From 96c22aba56cd9c3b35ffaeffbc06e3335b3f54a8 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Tue, 31 Mar 2026 08:52:09 +0100 Subject: [PATCH 07/18] Added dfe signin env vars to review deployment --- terraform/application/config/review.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/terraform/application/config/review.yml b/terraform/application/config/review.yml index 1cdbc6a..12d4eb3 100644 --- a/terraform/application/config/review.yml +++ b/terraform/application/config/review.yml @@ -1,2 +1,5 @@ --- -EXAMPLE_KEY: example value 1 +DfeSignIn__BaseUrl: https://test-api.signin.education.gov.uk/ +DfeSignIn__ClientId: CheckPerformanceData +DfeSignIn__Audience: signin.education.gov.uk +DfeSignIn__MetadataAddress: https://test-oidc.signin.education.gov.uk/.well-known/openid-configuration From 9c9d8d2c8dc6d0b7c3f2b41e2c651b3f5a949375 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Tue, 31 Mar 2026 09:04:29 +0100 Subject: [PATCH 08/18] Added startup code to make sure RequestPath, Status etc are present --- src/DfE.CheckPerformanceData.Web/Program.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index c3c7bc4..311af06 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -57,7 +57,16 @@ var app = builder.Build(); - app.UseSerilogRequestLogging(); + app.UseSerilogRequestLogging(options => + { + options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => + { + diagnosticContext.Set("RequestPath", httpContext.Request.Path); + diagnosticContext.Set("StatusCode", httpContext.Response.StatusCode); + diagnosticContext.Set("RequestMethod", httpContext.Request.Method); + diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString()); + }; + }); app.UseGovUkFrontend(); From 2c5f023107bc86cc0e80ce753fb18661b49b1f7b Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Tue, 31 Mar 2026 12:06:27 +0100 Subject: [PATCH 09/18] Updated readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af1ebd1..e333b7f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Prerequisites - ~~[Node.js](https://nodejs.org/en/download/) (for building web artefacts: (S)CSS, JS, etc.)~~ - IDE/Editor of choice (e.g., Visual Studio, Visual Studio Code, JetBrains Rider, etc.) - [Docker Desktop](https://docs.docker.com/desktop/) (for development time hosting of dependencies) - + + Clone the repository ```sh From ea9a211bf92ddb74ff5ff0a9eb0b4525aae30b19 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Wed, 1 Apr 2026 16:58:46 +0100 Subject: [PATCH 10/18] LAESTAB field for org dto --- .../DfESignInApiClient/OrganisationDto.cs | 5 +++ .../DfeSignInApiClient/DfeSignInApiClient.cs | 34 ++++++++++++++++++- .../Views/Secret/Index.cshtml | 5 +++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs b/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs index 9949d57..80b442f 100644 --- a/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs +++ b/src/DfE.CheckPerformanceData.Application/DfESignInApiClient/OrganisationDto.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace DfE.CheckPerformanceData.Application.DfESignInApiClient; public class OrganisationDto @@ -18,6 +20,9 @@ public class OrganisationDto public int? StatutoryHighAge { get; init; } public string? LegacyId { get; init; } public string? CompanyRegistrationNumber { get; init; } + + [JsonIgnore] + public string? LAESTAB { get; set; } } diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs index 29220ea..2a5d058 100644 --- a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignInApiClient/DfeSignInApiClient.cs @@ -1,4 +1,6 @@ using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using DfE.CheckPerformanceData.Application.DfESignInApiClient; namespace DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; @@ -14,8 +16,38 @@ public DfeSignInApiClient(HttpClient httpClient) public async Task GetOrganisationAsync(string userId, string organisationId) { - var userOrganisations = await _httpClient.GetFromJsonAsync>($"users/{userId}/organisations"); + //var organisationsJson = await _httpClient.GetStringAsync($"users/{userId}/organisations"); + var userOrganisations = await _httpClient.GetFromJsonAsync>($"users/{userId}/organisations", + new JsonSerializerOptions() + { + Converters = { new OrganisationDtoJsonConverter() } + }); return userOrganisations?.FirstOrDefault(o => o.Id == organisationId); } +} + +public class OrganisationDtoJsonConverter : JsonConverter +{ + public override OrganisationDto Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var root = document.RootElement; + + var dto = JsonSerializer.Deserialize(root.GetRawText(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + + + if (root.TryGetProperty("localAuthority", out var localAuthorityElement)) + { + var orgCode = localAuthorityElement.GetProperty("code").GetString(); + var orgId = root.GetProperty("establishmentNumber").GetString(); + + dto.LAESTAB = $"{orgCode}{orgId}"; + } + + return dto; + } + + public override void Write(Utf8JsonWriter writer, OrganisationDto value, JsonSerializerOptions options) => + throw new NotImplementedException(); } \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml index f1883e8..b114481 100644 --- a/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml +++ b/src/DfE.CheckPerformanceData.Web/Views/Secret/Index.cshtml @@ -13,6 +13,11 @@
ID
@Model.Organisation.Id
+ +
+
LAESTAB
+
@Model.Organisation.LAESTAB
+
Name
From 9ee7bf375c7cb8584bd8f76050e3268a243327a9 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Wed, 1 Apr 2026 17:00:05 +0100 Subject: [PATCH 11/18] Merge from stash --- .../IPortalDbContext.cs | 2 +- .../Entities/CheckingWindow.cs | 17 ++++++ .../Entities/Workflow.cs | 8 --- ...CheckPerformanceData.Infrastructure.csproj | 4 -- .../20260331100046_InitialCreate.Designer.cs | 56 +++++++++++++++++++ .../20260331100046_InitialCreate.cs | 39 +++++++++++++ .../PortalDbContextModelSnapshot.cs | 53 ++++++++++++++++++ .../Persistence/PortalDbContext.cs | 2 +- .../Extensions/MigrationExtensions.cs | 17 ++++++ src/DfE.CheckPerformanceData.Web/Program.cs | 3 + 10 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs delete mode 100644 src/DfE.CheckPerformanceData.Domain/Entities/Workflow.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.Designer.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs create mode 100644 src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs diff --git a/src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs b/src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs index e2cc0b0..6fbbeb7 100644 --- a/src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs +++ b/src/DfE.CheckPerformanceData.Application/IPortalDbContext.cs @@ -5,6 +5,6 @@ namespace DfE.CheckPerformanceData.Application; public interface IPortalDbContext { - DbSet Workflows { get; } + DbSet CheckingWindows { get; } Task SaveChangesAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs b/src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs new file mode 100644 index 0000000..4ad9e1f --- /dev/null +++ b/src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs @@ -0,0 +1,17 @@ +namespace DfE.CheckPerformanceData.Domain.Entities; + +public class CheckingWindow +{ + public int Id { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public OrganisationTypes OrganisationType { get; set; } + public string Title { get; set; } +} + +public enum OrganisationTypes +{ + KS2, + KS4, + Post16 +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Domain/Entities/Workflow.cs b/src/DfE.CheckPerformanceData.Domain/Entities/Workflow.cs deleted file mode 100644 index f4b4807..0000000 --- a/src/DfE.CheckPerformanceData.Domain/Entities/Workflow.cs +++ /dev/null @@ -1,8 +0,0 @@ - -namespace DfE.CheckPerformanceData.Domain.Entities; - -public class Workflow -{ - public int Id { get; set; } - public string Name { get; set; } -} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj b/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj index 594a606..721156e 100644 --- a/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfE.CheckPerformanceData.Infrastructure.csproj @@ -24,8 +24,4 @@ - - - - diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.Designer.cs b/src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.Designer.cs new file mode 100644 index 0000000..6029338 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.Designer.cs @@ -0,0 +1,56 @@ +// +using System; +using DfE.CheckPerformanceData.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DfE.CheckPerformanceData.Infrastructure.Migrations +{ + [DbContext(typeof(PortalDbContext))] + [Migration("20260331100046_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DfE.CheckPerformanceData.Domain.Entities.CheckingWindow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganisationType") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CheckingWindows"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.cs b/src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.cs new file mode 100644 index 0000000..addf8f7 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/Migrations/20260331100046_InitialCreate.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DfE.CheckPerformanceData.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CheckingWindows", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StartDate = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: false), + OrganisationType = table.Column(type: "integer", nullable: false), + Title = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CheckingWindows", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CheckingWindows"); + } + } +} diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs b/src/DfE.CheckPerformanceData.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs new file mode 100644 index 0000000..6106f74 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs @@ -0,0 +1,53 @@ +// +using System; +using DfE.CheckPerformanceData.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DfE.CheckPerformanceData.Infrastructure.Migrations +{ + [DbContext(typeof(PortalDbContext))] + partial class PortalDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DfE.CheckPerformanceData.Domain.Entities.CheckingWindow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganisationType") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CheckingWindows"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs b/src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs index aef69fe..2c96531 100644 --- a/src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs +++ b/src/DfE.CheckPerformanceData.Infrastructure/Persistence/PortalDbContext.cs @@ -6,5 +6,5 @@ namespace DfE.CheckPerformanceData.Infrastructure.Persistence; public class PortalDbContext(DbContextOptions options) : DbContext(options), IPortalDbContext { - public DbSet Workflows => Set(); + public DbSet CheckingWindows => Set(); } \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs b/src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs new file mode 100644 index 0000000..ec04840 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs @@ -0,0 +1,17 @@ +using DfE.CheckPerformanceData.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace DfE.CheckPerformanceData.Web.Extensions; + +public static class MigrationExtensions +{ + public static async Task MigrateDatabaseAsync(this WebApplication app) + { + if (!app.Environment.IsDevelopment()) + return; + + await using var scope = app.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + } +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index 311af06..2c3fb81 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -3,6 +3,7 @@ using DfE.CheckPerformanceData.Infrastructure.DfeSignIn; using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; using DfE.CheckPerformanceData.Infrastructure.Persistence; +using DfE.CheckPerformanceData.Web.Extensions; using GovUk.Frontend.AspNetCore; using Microsoft.EntityFrameworkCore; using Serilog; @@ -57,6 +58,8 @@ var app = builder.Build(); + await app.MigrateDatabaseAsync(); + app.UseSerilogRequestLogging(options => { options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => From 40205d40897a2bef12080fa0476a6115e9d8e029 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Wed, 1 Apr 2026 18:40:25 +0100 Subject: [PATCH 12/18] Added startup logging and force migration --- .../Extensions/MigrationExtensions.cs | 4 ++-- src/DfE.CheckPerformanceData.Web/Program.cs | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs b/src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs index ec04840..4b51087 100644 --- a/src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs +++ b/src/DfE.CheckPerformanceData.Web/Extensions/MigrationExtensions.cs @@ -7,8 +7,8 @@ public static class MigrationExtensions { public static async Task MigrateDatabaseAsync(this WebApplication app) { - if (!app.Environment.IsDevelopment()) - return; + // if (!app.Environment.IsDevelopment()) + // return; await using var scope = app.Services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index 2c3fb81..c27a3a4 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -24,18 +24,18 @@ builder.Host.UseSerilog((context, services, configuration) => { var isDevelopment = context.HostingEnvironment.IsDevelopment(); - + configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() - .WriteTo.Console(isDevelopment - ? new ExpressionTemplate( + .WriteTo.Console(isDevelopment + ? new ExpressionTemplate( "[{@t:HH:mm:ss} {@l:u3}] {SourceContext}\n {@m}\n{@x}", theme: TemplateTheme.Code) : new CompactJsonFormatter()); }); - + builder.Services .AddDfeApiClient(builder.Configuration) .AddDfeSignInAuthentication(builder.Configuration) @@ -59,7 +59,7 @@ var app = builder.Build(); await app.MigrateDatabaseAsync(); - + app.UseSerilogRequestLogging(options => { options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => @@ -70,7 +70,7 @@ diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString()); }; }); - + app.UseGovUkFrontend(); app.UseHealthChecks("/healthcheck"); @@ -101,6 +101,9 @@ } catch (Exception e) { - Console.WriteLine(e); - throw; + Log.Fatal(e, "Application terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); } \ No newline at end of file From cfdd334a0d199bff19a17bec52b9de0c048e4147 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sat, 4 Apr 2026 15:08:22 +0100 Subject: [PATCH 13/18] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e333b7f..ef517ef 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ git clone https://github.com/DFE-Digital/check-performance-data Build the C#/.NET solution ```sh dotnet build -``` +``` Confirm tests are passing locally ```sh From 995c5d7104023385afe3db5039625861b23449ee Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sat, 4 Apr 2026 15:10:26 +0100 Subject: [PATCH 14/18] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef517ef..ddaf7a2 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,4 @@ dotnet test ```sh docker compose up --build -d -``` \ No newline at end of file +``` \ No newline at end of file From bc1f1b616ab005450bafc72573834997432a9a8e Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sat, 4 Apr 2026 15:10:59 +0100 Subject: [PATCH 15/18] test --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddaf7a2..e333b7f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ git clone https://github.com/DFE-Digital/check-performance-data Build the C#/.NET solution ```sh dotnet build -``` +``` Confirm tests are passing locally ```sh @@ -30,4 +30,4 @@ dotnet test ```sh docker compose up --build -d -``` \ No newline at end of file +``` \ No newline at end of file From 885434a2dfbb9809281e40c21b6f603433e1b0b2 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sat, 4 Apr 2026 17:41:01 +0100 Subject: [PATCH 16/18] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e333b7f..22606e6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ dotnet build Confirm tests are passing locally ```sh dotnet test -``` +``` ```sh docker compose up --build -d From e43ebe8f2a0b7ba90bfc38ad053c85c4b40c24a0 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Sun, 5 Apr 2026 20:59:28 +0100 Subject: [PATCH 17/18] Claims Enrichment test --- docker-compose.yaml | 4 +-- .../IClaimsEnrichmentService.cs | 34 ++++++++++++++++++ .../Entities/CheckingWindow.cs | 6 ++-- .../DfeSignIn/DfeSignInAuthExtensions.cs | 19 ++++++++-- .../Seeding/DevDataSeeder.cs | 35 +++++++++++++++++++ .../Controllers/SecretController.cs | 10 +++++- src/DfE.CheckPerformanceData.Web/Program.cs | 12 +++++++ .../appsettings.json | 6 ---- 8 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 src/DfE.CheckPerformanceData.Application/ClaimsEnrichment/IClaimsEnrichmentService.cs create mode 100644 src/DfE.CheckPerformanceData.Infrastructure/Seeding/DevDataSeeder.cs diff --git a/docker-compose.yaml b/docker-compose.yaml index c0d8c99..d9f99be 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,7 @@ environment: - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Development} - ASPNETCORE_URLS=http://+:8080 - - ConnectionStrings__Postgres=${POSTGRES_CONNECTION_STRING:-Host=db;Database=helloworld;Username=postgres;Password=postgres} + - ConnectionStrings__Postgres=${POSTGRES_CONNECTION_STRING:-Host=db;Database=cypd;Username=postgres;Password=postgres} - ConnectionStrings__AzureStorage=${AZURE_STORAGE_CONNECTION_STRING:-DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://azurite:10001/devstoreaccount1;} - DfeSignIn__BaseUrl=${DFESIGNIN_BASEURL:-https://test-api.signin.education.gov.uk/} - DfeSignIn__ClientId=${DFESIGNIN_CLIENTID} @@ -52,7 +52,7 @@ container_name: cypd_db image: postgres:16 environment: - - POSTGRES_DB=helloworld + - POSTGRES_DB=cypd - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres ports: diff --git a/src/DfE.CheckPerformanceData.Application/ClaimsEnrichment/IClaimsEnrichmentService.cs b/src/DfE.CheckPerformanceData.Application/ClaimsEnrichment/IClaimsEnrichmentService.cs new file mode 100644 index 0000000..c89aa25 --- /dev/null +++ b/src/DfE.CheckPerformanceData.Application/ClaimsEnrichment/IClaimsEnrichmentService.cs @@ -0,0 +1,34 @@ +using System.Security.Claims; +using System.Text.Json.Nodes; +using DfE.CheckPerformanceData.Application.DfESignInApiClient; + +namespace DfE.CheckPerformanceData.Application.ClaimsEnrichment; + +public interface IClaimsEnrichmentService +{ + Task EnrichAsync(ClaimsIdentity identity); +} + + +public class ClaimsEnrichmentService: IClaimsEnrichmentService +{ + private readonly IDfESignInApiClient _apiClient; + + public ClaimsEnrichmentService(IDfESignInApiClient apiClient) + { + _apiClient = apiClient; + } + public async Task EnrichAsync(ClaimsIdentity identity) + { + var userid = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + var orgJson = identity.FindFirst("organisation")?.Value ?? "{}"; + var org = JsonNode.Parse(orgJson); + var orgId = org["id"]?.ToString() ?? string.Empty; + + var organisation = await _apiClient.GetOrganisationAsync(userid, orgId); + + identity.AddClaim(new Claim("low_age", organisation.StatutoryLowAge.ToString())); + identity.AddClaim(new Claim("high_age", organisation.StatutoryHighAge.ToString())); + } +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs b/src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs index 4ad9e1f..70f23ac 100644 --- a/src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs +++ b/src/DfE.CheckPerformanceData.Domain/Entities/CheckingWindow.cs @@ -11,7 +11,7 @@ public class CheckingWindow public enum OrganisationTypes { - KS2, - KS4, - Post16 + KS2, //3-11 + KS4, //11-16 + Post16 //16-18 } \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs index cad4519..31dd4a5 100644 --- a/src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs +++ b/src/DfE.CheckPerformanceData.Infrastructure/DfeSignIn/DfeSignInAuthExtensions.cs @@ -1,4 +1,6 @@ -using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; +using System.Security.Claims; +using DfE.CheckPerformanceData.Application.ClaimsEnrichment; +using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Configuration; @@ -55,7 +57,20 @@ public static IServiceCollection AddDfeSignInAuthentication(this IServiceCollect { //ctx.HttpContext.Response.Headers.Add("Cache-Control", "no-store"); return Task.CompletedTask; - }; + }; + + options.Events.OnUserInformationReceived = ctx => + { + return Task.CompletedTask; + }; + + options.Events.OnTokenValidated = async ctx => + { + var enrichmentService = ctx.HttpContext.RequestServices + .GetRequiredService(); + + await enrichmentService.EnrichAsync((ClaimsIdentity)ctx.Principal!.Identity!); + }; }); return services; diff --git a/src/DfE.CheckPerformanceData.Infrastructure/Seeding/DevDataSeeder.cs b/src/DfE.CheckPerformanceData.Infrastructure/Seeding/DevDataSeeder.cs new file mode 100644 index 0000000..14fc13b --- /dev/null +++ b/src/DfE.CheckPerformanceData.Infrastructure/Seeding/DevDataSeeder.cs @@ -0,0 +1,35 @@ +using DfE.CheckPerformanceData.Application; +using DfE.CheckPerformanceData.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace DfE.CheckPerformanceData.Infrastructure.Seeding; + +public class DevDataSeeder(IPortalDbContext dbContext) +{ + public async Task SeedAsync() + { + if (await dbContext.CheckingWindows.AnyAsync()) + return; + + await dbContext.CheckingWindows.AddRangeAsync( + new CheckingWindow() + { + Id = 1, + StartDate = DateTime.UtcNow.AddDays(-1), + EndDate = DateTime.UtcNow.AddDays(+13), + OrganisationType = OrganisationTypes.KS4, + Title = "KS4 June" + }, + new CheckingWindow() + { + Id = 2, + StartDate = DateTime.UtcNow.AddDays(-3), + EndDate = DateTime.UtcNow.AddDays(+11), + OrganisationType = OrganisationTypes.KS2, + Title = "KS2" + } + ); + + await dbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs index ac478f1..b26e587 100644 --- a/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs +++ b/src/DfE.CheckPerformanceData.Web/Controllers/SecretController.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using System.Text.Json.Nodes; +using DfE.CheckPerformanceData.Application; using DfE.CheckPerformanceData.Application.DfESignInApiClient; using DfE.CheckPerformanceData.Web.Controllers.ViewModels; using Microsoft.AspNetCore.Authentication; @@ -7,16 +8,19 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace DfE.CheckPerformanceData.Web.Controllers; public class SecretController : Controller { private readonly IDfESignInApiClient _dfeSignInApiClient; + private readonly IPortalDbContext _dbContext; - public SecretController(IDfESignInApiClient dfeSignInApiClient) + public SecretController(IDfESignInApiClient dfeSignInApiClient, IPortalDbContext dbContext) { _dfeSignInApiClient = dfeSignInApiClient; + _dbContext = dbContext; } [Authorize] @@ -35,6 +39,10 @@ public async Task Index() UserName = User.FindFirstValue(ClaimTypes.GivenName) + " " + User.FindFirstValue(ClaimTypes.Surname), Organisation = organisation }; + + var now = DateTime.UtcNow; + var currentWindowsForUser = await _dbContext.CheckingWindows.Where(w => w.StartDate <= now && w.EndDate >= now) + .AsNoTracking().ToListAsync(); return View(vm); } diff --git a/src/DfE.CheckPerformanceData.Web/Program.cs b/src/DfE.CheckPerformanceData.Web/Program.cs index c27a3a4..92bcec2 100644 --- a/src/DfE.CheckPerformanceData.Web/Program.cs +++ b/src/DfE.CheckPerformanceData.Web/Program.cs @@ -1,8 +1,10 @@ using Azure.Storage.Queues; using DfE.CheckPerformanceData.Application; +using DfE.CheckPerformanceData.Application.ClaimsEnrichment; using DfE.CheckPerformanceData.Infrastructure.DfeSignIn; using DfE.CheckPerformanceData.Infrastructure.DfeSignInApiClient; using DfE.CheckPerformanceData.Infrastructure.Persistence; +using DfE.CheckPerformanceData.Infrastructure.Seeding; using DfE.CheckPerformanceData.Web.Extensions; using GovUk.Frontend.AspNetCore; using Microsoft.EntityFrameworkCore; @@ -41,6 +43,11 @@ .AddDfeSignInAuthentication(builder.Configuration) .AddGovUkFrontend(options => options.Rebrand = true); + if (builder.Environment.IsDevelopment()) + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Postgres"))); @@ -82,6 +89,11 @@ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } + else + { + using var scope = app.Services.CreateScope(); + await scope.ServiceProvider.GetRequiredService().SeedAsync(); + } app.UseHttpsRedirection(); app.UseRouting(); diff --git a/src/DfE.CheckPerformanceData.Web/appsettings.json b/src/DfE.CheckPerformanceData.Web/appsettings.json index 463f06b..55aa94c 100644 --- a/src/DfE.CheckPerformanceData.Web/appsettings.json +++ b/src/DfE.CheckPerformanceData.Web/appsettings.json @@ -9,11 +9,5 @@ } } }, -// "Logging": { -// "LogLevel": { -// "Default": "Information", -// "Microsoft.AspNetCore": "Warning" -// } -// }, "AllowedHosts": "*" } From 729e4abcf7e36e3619df7a06a3432417c4037608 Mon Sep 17 00:00:00 2001 From: Dave Gouge Date: Mon, 6 Apr 2026 20:11:46 +0100 Subject: [PATCH 18/18] Altered compose file to allow easy starting of just background services --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index d9f99be..c52507a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,6 @@ services: web: + profiles: [web, all] container_name: cypd_web build: context: ./src @@ -24,6 +25,7 @@ - azurite rules_engine: + profiles: [rules_engine, all] container_name: cypd_rules_engine build: context: ./src