From fde9b485b8eff5f05793416f369c1fc2042ad3be Mon Sep 17 00:00:00 2001 From: James Gunn Date: Fri, 12 Apr 2024 14:43:47 +0100 Subject: [PATCH] Store verification metadata against a One Login user --- .../HttpResultActionResult.cs | 5 + .../OneLoginAuthenticationSchemeProvider.cs | 22 + .../Pages/DebugIdentity.cshtml.cs | 42 +- .../Pages/Found.cshtml.cs | 4 +- .../Pages/NotVerified.cshtml.cs | 9 +- .../SignInJourneyHelper.cs | 152 +- .../Postgres/Mappings/OneLoginUserMapping.cs | 24 + ...410164157_OneLoginUserVerified.Designer.cs | 2040 ++++++++++++++++ .../20240410164157_OneLoginUserVerified.cs | 39 + ...20240411145144_VerifiedDetails.Designer.cs | 2048 +++++++++++++++++ .../20240411145144_VerifiedDetails.cs | 38 + .../Migrations/TrsDbContextModelSnapshot.cs | 16 + .../DataStore/Postgres/Models/OneLoginUser.cs | 4 + .../OneLoginUserVerificationRoute.cs | 7 + .../Security/FakeOneLoginHandler.cs | 29 +- .../OidcTests.cs | 2 +- .../SignInTests.cs | 2 +- .../PageTests/CheckAnswersTests.cs | 54 +- .../PageTests/ConnectTests.cs | 36 +- .../PageTests/FoundTests.cs | 34 +- .../PageTests/NationalInsuranceNumberTests.cs | 134 +- .../PageTests/NotFoundTests.cs | 28 +- .../PageTests/NotVerifiedTests.cs | 31 +- .../PageTests/TrnTests.cs | 151 +- .../SignInJourneyHelperTests.cs | 122 +- .../TestBase.cs | 10 + .../TestData.CreateOneLoginUser.cs | 30 +- 27 files changed, 4732 insertions(+), 381 deletions(-) create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.Designer.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.Designer.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/OneLoginUserVerificationRoute.cs diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/HttpResultActionResult.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/HttpResultActionResult.cs index ed266e928..1697357b2 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/HttpResultActionResult.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/HttpResultActionResult.cs @@ -9,3 +9,8 @@ public Task ExecuteResultAsync(ActionContext context) return result.ExecuteAsync(context.HttpContext); } } + +public static class ResultExtensions +{ + public static IActionResult ToActionResult(this IResult result) => new HttpResultActionResult(result); +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Security/OneLoginAuthenticationSchemeProvider.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Security/OneLoginAuthenticationSchemeProvider.cs index e7be7efa1..adefff0ea 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Security/OneLoginAuthenticationSchemeProvider.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Security/OneLoginAuthenticationSchemeProvider.cs @@ -5,8 +5,11 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; +using TeachingRecordSystem.AuthorizeAccess.Infrastructure.FormFlow; using TeachingRecordSystem.Core.DataStore.Postgres; using TeachingRecordSystem.Core.DataStore.Postgres.Models; +using TeachingRecordSystem.FormFlow; +using static TeachingRecordSystem.AuthorizeAccess.Infrastructure.Security.FormFlowJourneySignInHandler; namespace TeachingRecordSystem.AuthorizeAccess.Infrastructure.Security; @@ -171,6 +174,25 @@ void IConfigureNamedOptions.Configure(string? name, OneLoginOpt return Task.CompletedTask; }; + options.Events.OnAccessDenied = async context => + { + // This handles the scenario where we've requested ID verification but One Login couldn't do it. + + if (context.Properties!.TryGetVectorOfTrust(out var vtr) && vtr == SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr && + context.Properties?.Items.TryGetValue(PropertyKeys.JourneyInstanceId, out var serializedInstanceId) == true && serializedInstanceId is not null) + { + context.HandleResponse(); + + var journeyInstanceId = JourneyInstanceId.Deserialize(serializedInstanceId); + + var signInJourneyHelper = context.HttpContext.RequestServices.GetRequiredService(); + var journeyInstance = (await signInJourneyHelper.UserInstanceStateProvider.GetSignInJourneyInstanceAsync(context.HttpContext, journeyInstanceId))!; + + var result = await signInJourneyHelper.OnUserVerificationWithOneLoginFailed(journeyInstance); + await result.ExecuteAsync(context.HttpContext); + } + }; + options.CoreIdentityClaimIssuerSigningKey = _coreIdentityIssuerSigningKey; options.CoreIdentityClaimIssuer = "https://identity.integration.account.gov.uk/"; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/DebugIdentity.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/DebugIdentity.cshtml.cs index dfcc935b2..71e7ca6f6 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/DebugIdentity.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/DebugIdentity.cshtml.cs @@ -17,6 +17,7 @@ namespace TeachingRecordSystem.AuthorizeAccess.Pages; public class DebugIdentityModel( TrsDbContext dbContext, SignInJourneyHelper helper, + IClock clock, IOptions optionsAccessor) : PageModel { private OneLoginUser? _oneLoginUser; @@ -105,9 +106,38 @@ public async Task OnPost() return this.PageWithErrors(); } - await JourneyInstance!.UpdateStateAsync(state => + if (DetachPerson && _oneLoginUser!.PersonId is not null) + { + _oneLoginUser.PersonId = null; + } + + if (_oneLoginUser!.PersonId is null) { if (IdentityVerified) + { + _oneLoginUser!.VerifiedOn = clock.UtcNow; + _oneLoginUser.VerificationRoute = OneLoginUserVerificationRoute.OneLogin; + _oneLoginUser.VerifiedNames = verifiedNames; + _oneLoginUser.VerifiedDatesOfBirth = verifiedDatesOfBirth; + } + else + { + _oneLoginUser!.VerifiedOn = null; + _oneLoginUser.VerificationRoute = null; + _oneLoginUser.VerifiedNames = null; + _oneLoginUser.VerifiedDatesOfBirth = null; + } + } + + await dbContext.SaveChangesAsync(); + + await JourneyInstance!.UpdateStateAsync(state => + { + if (_oneLoginUser!.PersonId is not null) + { + helper.Complete(state, _oneLoginUser.Person!.Trn!); + } + else if (IdentityVerified) { state.SetVerified(verifiedNames!, verifiedDatesOfBirth!); } @@ -117,14 +147,8 @@ public async Task OnPost() } }); - if (DetachPerson && _oneLoginUser?.PersonId is not null) - { - _oneLoginUser.PersonId = null; - await dbContext.SaveChangesAsync(); - } - - var nextPage = await helper.CreateOrUpdateOneLoginUser(JourneyInstance); - return new HttpResultActionResult(nextPage); + var nextPage = helper.GetNextPage(JourneyInstance); + return nextPage.ToActionResult(); } public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Found.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Found.cshtml.cs index 8c44e9535..b83695ae9 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Found.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Found.cshtml.cs @@ -14,7 +14,7 @@ public void OnGet() { } - public IActionResult OnPost() => Redirect(helper.GetNextPage(JourneyInstance!)); + public IActionResult OnPost() => helper.GetNextPage(JourneyInstance!).ToActionResult(); public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) { @@ -23,7 +23,7 @@ public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) if (state.AuthenticationTicket is null) { // Not matched - context.Result = Redirect(helper.GetNextPage(JourneyInstance)); + context.Result = helper.GetNextPage(JourneyInstance).ToActionResult(); } } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotVerified.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotVerified.cshtml.cs index 19725b0b7..3d4f13600 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotVerified.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotVerified.cshtml.cs @@ -5,7 +5,7 @@ namespace TeachingRecordSystem.AuthorizeAccess.Pages; [Journey(SignInJourneyState.JourneyName), RequireJourneyInstance] -public class NotVerifiedModel : PageModel +public class NotVerifiedModel(SignInJourneyHelper helper) : PageModel { public JourneyInstance? JourneyInstance { get; set; } @@ -17,7 +17,12 @@ public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) { var state = JourneyInstance!.State; - if (state.OneLoginAuthenticationTicket is null) + if (state.AuthenticationTicket is not null) + { + // Already matched to a Teaching Record + context.Result = Redirect(helper.GetSafeRedirectUri(JourneyInstance)); + } + else if (state.OneLoginAuthenticationTicket is null) { // Not authenticated with One Login context.Result = BadRequest(); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs index 7b28627ff..e06befd7e 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs @@ -1,5 +1,5 @@ +using System.Diagnostics; using System.Security.Claims; -using System.Text.Json; using GovUk.OneLogin.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.WebUtilities; @@ -42,93 +42,127 @@ public async Task OnSignedInWithOneLogin(JourneyInstance - { - state.Reset(); - state.OneLoginAuthenticationTicket = ticket; - state.AttemptedIdentityVerification = attemptedIdentityVerification; + var sub = ticket.Principal.FindFirstValue("sub") ?? throw new InvalidOperationException("No sub claim."); + var email = ticket.Principal.FindFirstValue("email") ?? throw new InvalidOperationException("No email claim."); - var vc = ticket.Principal.FindFirstValue("vc") is string vcStr ? JsonDocument.Parse(vcStr) : null; - var identityVerified = vc is not null; - if (identityVerified) - { - state.SetVerified( - verifiedNames: ticket.Principal.GetCoreIdentityNames().Select(n => n.NameParts.Select(part => part.Value).ToArray()).ToArray(), - verifiedDatesOfBirth: ticket.Principal.GetCoreIdentityBirthDates().Select(d => d.Value).ToArray()); - } - }); + if (vtr == AuthenticationOnlyVtr) + { + await OnUserAuthenticated(); + } + else + { + Debug.Assert(vtr == AuthenticationAndIdentityVerificationVtr); + Debug.Assert(journeyInstance.State.OneLoginAuthenticationTicket is not null); + await OnUserVerified(); + } if (ShowDebugPages) { return Results.Redirect(LinkGenerator.DebugIdentity(journeyInstance.InstanceId)); } - return await CreateOrUpdateOneLoginUser(journeyInstance); - } + return GetNextPage(journeyInstance); - public async Task CreateOrUpdateOneLoginUser(JourneyInstance journeyInstance) - { - if (journeyInstance.State.OneLoginAuthenticationTicket is null) + async Task OnUserAuthenticated() { - throw new InvalidOperationException($"{nameof(journeyInstance.State.OneLoginAuthenticationTicket)} is not set."); - } + var oneLoginUser = await dbContext.OneLoginUsers + .Include(u => u.Person) + .SingleOrDefaultAsync(u => u.Subject == sub); - var subject = journeyInstance.State.OneLoginAuthenticationTicket.Principal.FindFirstValue("sub") ?? throw new InvalidOperationException("No sub claim."); - var email = journeyInstance.State.OneLoginAuthenticationTicket.Principal.FindFirstValue("email") ?? throw new InvalidOperationException("No email claim."); - var vc = journeyInstance.State.OneLoginAuthenticationTicket.Principal.FindFirstValue("vc") is string vcStr ? JsonDocument.Parse(vcStr) : null; + if (oneLoginUser is null) + { + oneLoginUser = new() + { + Subject = sub, + Email = email, + FirstOneLoginSignIn = clock.UtcNow, + LastOneLoginSignIn = clock.UtcNow + }; + dbContext.OneLoginUsers.Add(oneLoginUser); + } + else + { + oneLoginUser.LastOneLoginSignIn = clock.UtcNow; - var oneLoginUser = await dbContext.OneLoginUsers - .Include(o => o.Person) - .SingleOrDefaultAsync(o => o.Subject == subject); + // Email may have changed since the last sign in - ensure we update it. + // TODO Should we emit an event if it has changed? + oneLoginUser.Email = email; - if (oneLoginUser is not null) - { - oneLoginUser.LastOneLoginSignIn = clock.UtcNow; - oneLoginUser.Email = email; + if (oneLoginUser.PersonId is not null) + { + oneLoginUser.LastSignIn = clock.UtcNow; + } + } - if (oneLoginUser.Person is not null) - { - oneLoginUser.LastSignIn = clock.UtcNow; - await dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); - await journeyInstance.UpdateStateAsync(state => CreateAndAssignPrincipal(state, oneLoginUser.Person.Trn!)); - } + await journeyInstance.UpdateStateAsync(state => + { + state.Reset(); + state.OneLoginAuthenticationTicket = ticket; + + if (oneLoginUser.VerificationRoute is not null) + { + Debug.Assert(oneLoginUser.VerifiedNames is not null); + Debug.Assert(oneLoginUser.VerifiedDatesOfBirth is not null); + state.SetVerified(oneLoginUser.VerifiedNames!, oneLoginUser.VerifiedDatesOfBirth!); + } + + if (oneLoginUser.Person?.Trn is string trn && !ShowDebugPages) + { + Complete(state, trn); + } + }); } - else + + async Task OnUserVerified() { - oneLoginUser = new() + var verifiedNames = ticket.Principal.GetCoreIdentityNames().Select(n => n.NameParts.Select(part => part.Value).ToArray()).ToArray(); + var verifiedDatesOfBirth = ticket.Principal.GetCoreIdentityBirthDates().Select(d => d.Value).ToArray(); + + var oneLoginUser = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == sub); + oneLoginUser.VerifiedOn = clock.UtcNow; + oneLoginUser.VerificationRoute = OneLoginUserVerificationRoute.OneLogin; + oneLoginUser.VerifiedNames = verifiedNames; + oneLoginUser.VerifiedDatesOfBirth = verifiedDatesOfBirth; + await dbContext.SaveChangesAsync(); + + await journeyInstance.UpdateStateAsync(state => { - Subject = subject, - Email = email, - FirstOneLoginSignIn = clock.UtcNow, - LastOneLoginSignIn = clock.UtcNow - }; - dbContext.OneLoginUsers.Add(oneLoginUser); - } + state.OneLoginAuthenticationTicket = ticket; + state.AttemptedIdentityVerification = true; - await dbContext.SaveChangesAsync(); + state.SetVerified(verifiedNames, verifiedDatesOfBirth); + }); + } + } - if (oneLoginUser.PersonId is null && !journeyInstance.State.IdentityVerified && !journeyInstance.State.AttemptedIdentityVerification) + public async Task OnUserVerificationWithOneLoginFailed(JourneyInstance journeyInstance) + { + await journeyInstance.UpdateStateAsync(state => { - return VerifyIdentityWithOneLogin(journeyInstance); - } + state.AttemptedIdentityVerification = true; + }); - return Results.Redirect(GetNextPage(journeyInstance)); + return GetNextPage(journeyInstance); } - public string GetNextPage(JourneyInstance journeyInstance) => journeyInstance.State switch + public IResult GetNextPage(JourneyInstance journeyInstance) => journeyInstance.State switch { // Authentication is complete - { AuthenticationTicket: not null } => GetSafeRedirectUri(journeyInstance), + { AuthenticationTicket: not null } => Results.Redirect(GetSafeRedirectUri(journeyInstance)), // Authenticated with OneLogin, identity verification succeeded, not yet matched to teaching record { OneLoginAuthenticationTicket: not null, IdentityVerified: true, AuthenticationTicket: null } => - LinkGenerator.Connect(journeyInstance.InstanceId), + Results.Redirect(LinkGenerator.Connect(journeyInstance.InstanceId)), + + // Authenticated with OneLogin, not yet verified + { OneLoginAuthenticationTicket: not null, IdentityVerified: false, AttemptedIdentityVerification: false } => + VerifyIdentityWithOneLogin(journeyInstance), // Authenticated with OneLogin, identity verification failed - { OneLoginAuthenticationTicket: not null, IdentityVerified: false } => LinkGenerator.NotVerified(journeyInstance.InstanceId), + { OneLoginAuthenticationTicket: not null, IdentityVerified: false } => Results.Redirect(LinkGenerator.NotVerified(journeyInstance.InstanceId)), _ => throw new InvalidOperationException("Cannot determine next page.") }; @@ -173,7 +207,7 @@ public async Task TryMatchToTeachingRecord(JourneyInstance CreateAndAssignPrincipal(state, matchedTrn)); + await journeyInstance.UpdateStateAsync(state => Complete(state, matchedTrn)); return true; } @@ -187,7 +221,7 @@ public async Task TryMatchToTeachingRecord(JourneyInstance builder) builder.Property(o => o.Subject).HasMaxLength(200); builder.Property(o => o.Email).HasMaxLength(200); builder.HasOne(o => o.Person).WithOne().HasForeignKey(o => o.PersonId); + builder.Property(o => o.VerifiedNames).HasColumnType("jsonb").HasConversion( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize(v, (JsonSerializerOptions?)null), + new ValueComparer( + (a, b) => (a == null && b == null) || (a != null && b != null && a.SequenceEqual(b, new StringArrayEqualityComparer())), + v => HashCode.Combine(v.Select(names => string.Join(" ", names))))); + builder.Property(o => o.VerifiedDatesOfBirth).HasColumnType("jsonb").HasConversion( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize(v, (JsonSerializerOptions?)null), + new ValueComparer( + (a, b) => (a == null && b == null) || (a != null && b != null && a.SequenceEqual(b)), + v => HashCode.Combine(v))); } } + +file class StringArrayEqualityComparer : IEqualityComparer +{ + public bool Equals(string[]? x, string[]? y) => + (x is null && y is null) || + (x is not null && y is not null && x.SequenceEqual(y)); + + public int GetHashCode([DisallowNull] string[] obj) => HashCode.Combine(obj); +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.Designer.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.Designer.cs new file mode 100644 index 000000000..5981df333 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.Designer.cs @@ -0,0 +1,2040 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TeachingRecordSystem.Core.DataStore.Postgres; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + [DbContext(typeof(TrsDbContext))] + [Migration("20240410164157_OneLoginUserVerified")] + partial class OneLoginUserVerified + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("application_type"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_type"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("consent_type"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("DisplayNames") + .HasColumnType("text") + .HasColumnName("display_names"); + + b.Property("JsonWebKeySet") + .HasColumnType("text") + .HasColumnName("json_web_key_set"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text") + .HasColumnName("post_logout_redirect_uris"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("RedirectUris") + .HasColumnType("text") + .HasColumnName("redirect_uris"); + + b.Property("Requirements") + .HasColumnType("text") + .HasColumnName("requirements"); + + b.Property("Settings") + .HasColumnType("text") + .HasColumnName("settings"); + + b.HasKey("Id") + .HasName("pk_oidc_applications"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_oidc_applications_client_id"); + + b.ToTable("oidc_applications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_date"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("Scopes") + .HasColumnType("text") + .HasColumnName("scopes"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("status"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("subject"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_oidc_authorizations"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type") + .HasDatabaseName("ix_oidc_authorizations_application_id_status_subject_type"); + + b.ToTable("oidc_authorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Descriptions") + .HasColumnType("text") + .HasColumnName("descriptions"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("DisplayNames") + .HasColumnType("text") + .HasColumnName("display_names"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("Resources") + .HasColumnType("text") + .HasColumnName("resources"); + + b.HasKey("Id") + .HasName("pk_oidc_scopes"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_oidc_scopes_name"); + + b.ToTable("oidc_scopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("AuthorizationId") + .HasColumnType("uuid") + .HasColumnName("authorization_id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_date"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.Property("Payload") + .HasColumnType("text") + .HasColumnName("payload"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("redemption_date"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("reference_id"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("status"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("subject"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_oidc_tokens"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasDatabaseName("ix_oidc_tokens_reference_id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type") + .HasDatabaseName("ix_oidc_tokens_application_id_status_subject_type"); + + b.ToTable("oidc_tokens", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("api_key_id"); + + b.Property("ApplicationUserId") + .HasColumnType("uuid") + .HasColumnName("application_user_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("key"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("ApiKeyId") + .HasName("pk_api_keys"); + + b.HasIndex("ApplicationUserId") + .HasDatabaseName("ix_api_keys_application_user_id"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_api_keys_key"); + + b.ToTable("api_keys", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EntityChangesJournal", b => + { + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("EntityLogicalName") + .HasColumnType("text") + .HasColumnName("entity_logical_name"); + + b.Property("DataToken") + .HasColumnType("text") + .HasColumnName("data_token"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_updated"); + + b.Property("LastUpdatedBy") + .HasColumnType("text") + .HasColumnName("last_updated_by"); + + b.Property("NextQueryPageNumber") + .HasColumnType("integer") + .HasColumnName("next_query_page_number"); + + b.Property("NextQueryPageSize") + .HasColumnType("integer") + .HasColumnName("next_query_page_size"); + + b.Property("NextQueryPagingCookie") + .HasColumnType("text") + .HasColumnName("next_query_paging_cookie"); + + b.HasKey("Key", "EntityLogicalName") + .HasName("pk_entity_changes_journals"); + + b.ToTable("entity_changes_journals", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", b => + { + b.Property("EstablishmentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("establishment_id"); + + b.Property("Address3") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("address3") + .UseCollation("case_insensitive"); + + b.Property("County") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("county") + .UseCollation("case_insensitive"); + + b.Property("EstablishmentName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("establishment_name") + .UseCollation("case_insensitive"); + + b.Property("EstablishmentNumber") + .HasMaxLength(4) + .HasColumnType("character(4)") + .HasColumnName("establishment_number") + .IsFixedLength(); + + b.Property("EstablishmentSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("establishment_source_id"); + + b.Property("EstablishmentStatusCode") + .HasColumnType("integer") + .HasColumnName("establishment_status_code"); + + b.Property("EstablishmentStatusName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_status_name"); + + b.Property("EstablishmentTypeCode") + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("establishment_type_code"); + + b.Property("EstablishmentTypeGroupCode") + .HasColumnType("integer") + .HasColumnName("establishment_type_group_code"); + + b.Property("EstablishmentTypeGroupName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_type_group_name"); + + b.Property("EstablishmentTypeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("establishment_type_name") + .UseCollation("case_insensitive"); + + b.Property("LaCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("la_code") + .IsFixedLength(); + + b.Property("LaName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("la_name") + .UseCollation("case_insensitive"); + + b.Property("Locality") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("locality") + .UseCollation("case_insensitive"); + + b.Property("Postcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("postcode") + .UseCollation("case_insensitive"); + + b.Property("Street") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("street") + .UseCollation("case_insensitive"); + + b.Property("Town") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("town") + .UseCollation("case_insensitive"); + + b.Property("Urn") + .HasMaxLength(6) + .HasColumnType("integer") + .HasColumnName("urn") + .IsFixedLength(); + + b.HasKey("EstablishmentId") + .HasName("pk_establishments"); + + b.HasIndex("EstablishmentSourceId") + .HasDatabaseName("ix_establishment_establishment_source_id"); + + b.HasIndex("Urn") + .IsUnique() + .HasDatabaseName("ix_establishment_urn"); + + b.HasIndex("LaCode", "EstablishmentNumber") + .HasDatabaseName("ix_establishment_la_code_establishment_number"); + + b.ToTable("establishments", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EstablishmentSource", b => + { + b.Property("EstablishmentSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("establishment_source_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EstablishmentSourceId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name") + .UseCollation("case_insensitive"); + + b.HasKey("EstablishmentSourceId") + .HasName("pk_establishment_sources"); + + b.ToTable("establishment_sources", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Event", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("event_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("EventName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("event_name"); + + b.Property("Inserted") + .HasColumnType("timestamp with time zone") + .HasColumnName("inserted"); + + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.HasKey("EventId") + .HasName("pk_events"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_events_key") + .HasFilter("key is not null"); + + b.HasIndex("Payload") + .HasDatabaseName("ix_events_payload"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Payload"), "gin"); + + b.HasIndex("PersonId", "EventName") + .HasDatabaseName("ix_events_person_id_event_name") + .HasFilter("person_id is not null"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("PersonId", "EventName"), new[] { "Payload" }); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Property("EytsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("EytsAwardedEmailsJobId") + .HasName("pk_eyts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_eyts_awarded_emails_jobs_executed_utc"); + + b.ToTable("eyts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.Property("EytsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("EytsAwardedEmailsJobId", "PersonId") + .HasName("pk_eyts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_eyts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("eyts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Property("InductionCompletedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InductionCompletedEmailsJobId") + .HasName("pk_induction_completed_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_induction_completed_emails_jobs_executed_utc"); + + b.ToTable("induction_completed_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.Property("InductionCompletedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InductionCompletedEmailsJobId", "PersonId") + .HasName("pk_induction_completed_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_induction_completed_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("induction_completed_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InternationalQtsAwardedEmailsJobId") + .HasName("pk_international_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_international_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("international_qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InternationalQtsAwardedEmailsJobId", "PersonId") + .HasName("pk_international_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_international_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("international_qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.JourneyState", b => + { + b.Property("InstanceId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("instance_id"); + + b.Property("Completed") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("user_id"); + + b.HasKey("InstanceId") + .HasName("pk_journey_states"); + + b.ToTable("journey_states", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", b => + { + b.Property("MandatoryQualificationProviderId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("mandatory_qualification_provider_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("MandatoryQualificationProviderId") + .HasName("pk_mandatory_qualification_providers"); + + b.ToTable("mandatory_qualification_providers", (string)null); + + b.HasData( + new + { + MandatoryQualificationProviderId = new Guid("e28ea41d-408d-4c89-90cc-8b9b04ac68f5"), + Name = "University of Birmingham" + }, + new + { + MandatoryQualificationProviderId = new Guid("89f9a1aa-3d68-4985-a4ce-403b6044c18c"), + Name = "University of Leeds" + }, + new + { + MandatoryQualificationProviderId = new Guid("aa5c300e-3b7c-456c-8183-3520b3d55dca"), + Name = "University of Manchester" + }, + new + { + MandatoryQualificationProviderId = new Guid("f417e73e-e2ad-40eb-85e3-55865be7f6be"), + Name = "Mary Hare School / University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("fbf22e04-b274-4c80-aba8-79fb6a7a32ce"), + Name = "University of Edinburgh" + }, + new + { + MandatoryQualificationProviderId = new Guid("26204149-349c-4ad6-9466-bb9b83723eae"), + Name = "Liverpool John Moores University" + }, + new + { + MandatoryQualificationProviderId = new Guid("0c30f666-647c-4ea8-8883-0fc6010b56be"), + Name = "University of Oxford/Oxford Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d0e6d54c-5e90-438a-945d-f97388c2b352"), + Name = "University of Cambridge" + }, + new + { + MandatoryQualificationProviderId = new Guid("aec32252-ef25-452e-a358-34a04e03369c"), + Name = "University of Newcastle-upon-Tyne" + }, + new + { + MandatoryQualificationProviderId = new Guid("d9ee7054-7fde-4cfd-9a5e-4b99511d1b3d"), + Name = "University of Plymouth" + }, + new + { + MandatoryQualificationProviderId = new Guid("707d58ca-1953-413b-9a46-41e9b0be885e"), + Name = "University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("3fc648a7-18e4-49e7-8a4b-1612616b72d5"), + Name = "University of London" + }, + new + { + MandatoryQualificationProviderId = new Guid("374dceb8-8224-45b8-b7dc-a6b0282b1065"), + Name = "Bristol Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d4fc958b-21de-47ec-9f03-36ae237a1b11"), + Name = "University College, Swansea" + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.NameSynonyms", b => + { + b.Property("NameSynonymsId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("name_synonyms_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NameSynonymsId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .UseCollation("case_insensitive"); + + b.Property("Synonyms") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("synonyms") + .UseCollation("case_insensitive"); + + b.HasKey("NameSynonymsId") + .HasName("pk_name_synonyms"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_name_synonyms_name"); + + b.ToTable("name_synonyms", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", b => + { + b.Property("Subject") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("subject"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("FirstOneLoginSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_one_login_sign_in"); + + b.Property("FirstSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_sign_in"); + + b.Property("LastOneLoginSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_one_login_sign_in"); + + b.Property("LastSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("VerificationRoute") + .HasColumnType("integer") + .HasColumnName("verification_route"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_on"); + + b.HasKey("Subject") + .HasName("pk_one_login_users"); + + b.ToTable("one_login_users", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtContactId") + .HasColumnType("uuid") + .HasColumnName("dqt_contact_id"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_first_name") + .UseCollation("case_insensitive"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_last_name") + .UseCollation("case_insensitive"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtMiddleName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_middle_name") + .UseCollation("case_insensitive"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("EmailAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("email_address") + .UseCollation("case_insensitive"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("first_name") + .UseCollation("case_insensitive"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("last_name") + .UseCollation("case_insensitive"); + + b.Property("MiddleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("middle_name") + .UseCollation("case_insensitive"); + + b.Property("NationalInsuranceNumber") + .HasMaxLength(9) + .HasColumnType("character(9)") + .HasColumnName("national_insurance_number") + .IsFixedLength(); + + b.Property("Trn") + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("PersonId") + .HasName("pk_persons"); + + b.HasIndex("DqtContactId") + .IsUnique() + .HasDatabaseName("ix_persons_dqt_contact_id") + .HasFilter("dqt_contact_id is not null"); + + b.HasIndex("Trn") + .IsUnique() + .HasDatabaseName("ix_persons_trn") + .HasFilter("trn is not null"); + + b.ToTable("persons", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonEmployment", b => + { + b.Property("PersonEmploymentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("person_employment_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("EmploymentType") + .HasColumnType("integer") + .HasColumnName("employment_type"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("EstablishmentId") + .HasColumnType("uuid") + .HasColumnName("establishment_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("PersonEmploymentId") + .HasName("pk_person_employments"); + + b.HasIndex("EstablishmentId") + .HasDatabaseName("ix_person_employments_establishment_id"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_employments_person_id"); + + b.ToTable("person_employments", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonSearchAttribute", b => + { + b.Property("PersonSearchAttributeId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("person_search_attribute_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersonSearchAttributeId")); + + b.Property("AttributeKey") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("attribute_key") + .UseCollation("case_insensitive"); + + b.Property("AttributeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("attribute_type") + .UseCollation("case_insensitive"); + + b.Property("AttributeValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("attribute_value") + .UseCollation("case_insensitive"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.HasKey("PersonSearchAttributeId") + .HasName("pk_person_search_attributes"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_search_attributes_person_id"); + + b.HasIndex("AttributeType", "AttributeValue") + .HasDatabaseName("ix_person_search_attributes_attribute_type_and_value"); + + b.ToTable("person_search_attributes", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Property("QtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("QtsAwardedEmailsJobId") + .HasName("pk_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.Property("QtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("QtsAwardedEmailsJobId", "PersonId") + .HasName("pk_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.Property("QualificationId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qualification_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtQualificationId") + .HasColumnType("uuid") + .HasColumnName("dqt_qualification_id"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("QualificationType") + .HasColumnType("integer") + .HasColumnName("qualification_type"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("QualificationId") + .HasName("pk_qualifications"); + + b.HasIndex("DqtQualificationId") + .IsUnique() + .HasDatabaseName("ix_qualifications_dqt_qualification_id") + .HasFilter("dqt_qualification_id is not null"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_qualifications_person_id"); + + b.ToTable("qualifications", (string)null); + + b.HasDiscriminator("QualificationType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", b => + { + b.Property("TpsCsvExtractId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Filename") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("filename"); + + b.HasKey("TpsCsvExtractId") + .HasName("pk_tps_csv_extracts"); + + b.ToTable("tps_csv_extracts", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractItem", b => + { + b.Property("TpsCsvExtractItemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_item_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("DateOfDeath") + .HasColumnType("date") + .HasColumnName("date_of_death"); + + b.Property("EmploymentEndDate") + .HasColumnType("date") + .HasColumnName("employment_end_date"); + + b.Property("EmploymentStartDate") + .HasColumnType("date") + .HasColumnName("employment_start_date"); + + b.Property("EmploymentType") + .HasColumnType("integer") + .HasColumnName("employment_type"); + + b.Property("EstablishmentEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_email_address"); + + b.Property("EstablishmentNumber") + .HasMaxLength(4) + .HasColumnType("character(4)") + .HasColumnName("establishment_number") + .IsFixedLength(); + + b.Property("EstablishmentPostcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("establishment_postcode"); + + b.Property("ExtractDate") + .HasColumnType("date") + .HasColumnName("extract_date"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("gender"); + + b.Property("LocalAuthorityCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("local_authority_code") + .IsFixedLength(); + + b.Property("MemberEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_email_address"); + + b.Property("MemberId") + .HasColumnType("integer") + .HasColumnName("member_id"); + + b.Property("MemberPostcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("member_postcode"); + + b.Property("NationalInsuranceNumber") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("character(9)") + .HasColumnName("national_insurance_number") + .IsFixedLength(); + + b.Property("Result") + .HasColumnType("integer") + .HasColumnName("result"); + + b.Property("TpsCsvExtractId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("TpsCsvExtractLoadItemId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_load_item_id"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.Property("WithdrawlIndicator") + .HasMaxLength(1) + .HasColumnType("character(1)") + .HasColumnName("withdrawl_indicator") + .IsFixedLength(); + + b.HasKey("TpsCsvExtractItemId") + .HasName("pk_tps_csv_extract_items"); + + b.HasIndex("TpsCsvExtractId") + .HasDatabaseName("ix_tps_csv_extract_items_tps_csv_extract_id"); + + b.HasIndex("TpsCsvExtractLoadItemId") + .HasDatabaseName("ix_tps_csv_extract_items_tps_csv_extract_load_item_id"); + + b.HasIndex("Trn") + .HasDatabaseName("ix_tps_csv_extract_items_trn"); + + b.HasIndex("LocalAuthorityCode", "EstablishmentNumber") + .HasDatabaseName("ix_tps_csv_extract_items_la_code_establishment_number"); + + b.ToTable("tps_csv_extract_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", b => + { + b.Property("TpsCsvExtractLoadItemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_load_item_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("DateOfBirth") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("date_of_birth"); + + b.Property("DateOfDeath") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("date_of_death"); + + b.Property("EmploymentEndDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("employment_end_date"); + + b.Property("EmploymentStartDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("employment_start_date"); + + b.Property("Errors") + .HasColumnType("integer") + .HasColumnName("errors"); + + b.Property("EstablishmentEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_email_address"); + + b.Property("EstablishmentNumber") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_number"); + + b.Property("EstablishmentPostcode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_postcode"); + + b.Property("ExtractDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("extract_date"); + + b.Property("FullOrPartTimeIndicator") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("full_or_part_time_indicator"); + + b.Property("Gender") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("gender"); + + b.Property("LocalAuthorityCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("local_authority_code"); + + b.Property("MemberEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_email_address"); + + b.Property("MemberId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_id"); + + b.Property("MemberPostcode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_postcode"); + + b.Property("NationalInsuranceNumber") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("national_insurance_number"); + + b.Property("TpsCsvExtractId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("Trn") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("trn"); + + b.Property("WithdrawlIndicator") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("withdrawl_indicator"); + + b.HasKey("TpsCsvExtractLoadItemId") + .HasName("pk_tps_csv_extract_load_items"); + + b.ToTable("tps_csv_extract_load_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TrnRequest", b => + { + b.Property("TrnRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("trn_request_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TrnRequestId")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_id"); + + b.Property("IdentityUserId") + .HasColumnType("uuid") + .HasColumnName("identity_user_id"); + + b.Property("LinkedToIdentity") + .HasColumnType("boolean") + .HasColumnName("linked_to_identity"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("request_id"); + + b.Property("TeacherId") + .HasColumnType("uuid") + .HasColumnName("teacher_id"); + + b.Property("TrnToken") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("trn_token"); + + b.HasKey("TrnRequestId") + .HasName("pk_trn_requests"); + + b.HasIndex("ClientId", "RequestId") + .IsUnique() + .HasDatabaseName("ix_trn_requests_client_id_request_id"); + + b.ToTable("trn_requests", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UserType") + .HasColumnType("integer") + .HasColumnName("user_type"); + + b.HasKey("UserId") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + + b.HasDiscriminator("UserType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification"); + + b.Property("DqtMqEstablishmentId") + .HasColumnType("uuid") + .HasColumnName("dqt_mq_establishment_id"); + + b.Property("DqtSpecialismId") + .HasColumnType("uuid") + .HasColumnName("dqt_specialism_id"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("ProviderId") + .HasColumnType("uuid") + .HasColumnName("mq_provider_id"); + + b.Property("Specialism") + .HasColumnType("integer") + .HasColumnName("mq_specialism"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("mq_status"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("ApiRoles") + .HasColumnType("varchar[]") + .HasColumnName("api_roles"); + + b.Property("ClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("client_secret"); + + b.Property("IsOidcClient") + .HasColumnType("boolean") + .HasColumnName("is_oidc_client"); + + b.Property("OneLoginAuthenticationSchemeName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_authentication_scheme_name"); + + b.Property("OneLoginClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_client_id"); + + b.Property("OneLoginPostLogoutRedirectUriPath") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("one_login_post_logout_redirect_uri_path"); + + b.Property("OneLoginPrivateKeyPem") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("one_login_private_key_pem"); + + b.Property("OneLoginRedirectUriPath") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("one_login_redirect_uri_path"); + + b.Property>("PostLogoutRedirectUris") + .HasColumnType("varchar[]") + .HasColumnName("post_logout_redirect_uris"); + + b.Property>("RedirectUris") + .HasColumnType("varchar[]") + .HasColumnName("redirect_uris"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_users_client_id") + .HasFilter("client_id is not null"); + + b.HasIndex("OneLoginAuthenticationSchemeName") + .IsUnique() + .HasDatabaseName("ix_users_one_login_authentication_scheme_name") + .HasFilter("one_login_authentication_scheme_name is not null"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.SystemUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.HasDiscriminator().HasValue(3); + + b.HasData( + new + { + UserId = new Guid("a81394d1-a498-46d8-af3e-e077596ab303"), + Active = true, + Name = "System", + UserType = 0 + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.User", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("AzureAdUserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("azure_ad_user_id"); + + b.Property("DqtUserId") + .HasColumnType("uuid") + .HasColumnName("dqt_user_id"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .UseCollation("case_insensitive"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("varchar[]") + .HasColumnName("roles"); + + b.HasIndex("AzureAdUserId") + .IsUnique() + .HasDatabaseName("ix_users_azure_ad_user_id"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId") + .HasConstraintName("fk_oidc_authorizations_oidc_applications_application_id"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId") + .HasConstraintName("fk_oidc_tokens_oidc_applications_application_id"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId") + .HasConstraintName("fk_oidc_tokens_oidc_authorizations_authorization_id"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", "ApplicationUser") + .WithMany("ApiKeys") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_key_application_user"); + + b.Navigation("ApplicationUser"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.EstablishmentSource", null) + .WithMany() + .HasForeignKey("EstablishmentSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_establishments_establishment_source_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", "EytsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("EytsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_eyts_awarded_emails_job_items_eyts_awarded_emails_jobs_eyts"); + + b.Navigation("EytsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", "InductionCompletedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InductionCompletedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_induction_completed_emails_job_items_induction_completed_em"); + + b.Navigation("InductionCompletedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", "InternationalQtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InternationalQtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_international_qts_awarded_emails_job_items_international_qt"); + + b.Navigation("InternationalQtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", "Person") + .WithOne() + .HasForeignKey("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", "PersonId") + .HasConstraintName("fk_one_login_users_persons_person_id"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonEmployment", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", null) + .WithMany() + .HasForeignKey("EstablishmentId") + .HasConstraintName("fk_person_employments_establishment_id"); + + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_person_employments_person_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", "QtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("QtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qts_awarded_emails_job_items_qts_awarded_emails_jobs_qts_aw"); + + b.Navigation("QtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qualifications_person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", null) + .WithMany() + .HasForeignKey("TpsCsvExtractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_items_tps_csv_extract_id"); + + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", null) + .WithMany() + .HasForeignKey("TpsCsvExtractLoadItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_items_tps_csv_extract_load_item_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", null) + .WithMany() + .HasForeignKey("TpsCsvExtractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_load_items_tps_csv_extract_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .HasConstraintName("fk_qualifications_mandatory_qualification_provider"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.Navigation("ApiKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.cs new file mode 100644 index 000000000..b47a58d19 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240410164157_OneLoginUserVerified.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + /// + public partial class OneLoginUserVerified : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "verification_route", + table: "one_login_users", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "verified_on", + table: "one_login_users", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "verification_route", + table: "one_login_users"); + + migrationBuilder.DropColumn( + name: "verified_on", + table: "one_login_users"); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.Designer.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.Designer.cs new file mode 100644 index 000000000..e4364350e --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.Designer.cs @@ -0,0 +1,2048 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TeachingRecordSystem.Core.DataStore.Postgres; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + [DbContext(typeof(TrsDbContext))] + [Migration("20240411145144_VerifiedDetails")] + partial class VerifiedDetails + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("application_type"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_type"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("consent_type"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("DisplayNames") + .HasColumnType("text") + .HasColumnName("display_names"); + + b.Property("JsonWebKeySet") + .HasColumnType("text") + .HasColumnName("json_web_key_set"); + + b.Property("Permissions") + .HasColumnType("text") + .HasColumnName("permissions"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text") + .HasColumnName("post_logout_redirect_uris"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("RedirectUris") + .HasColumnType("text") + .HasColumnName("redirect_uris"); + + b.Property("Requirements") + .HasColumnType("text") + .HasColumnName("requirements"); + + b.Property("Settings") + .HasColumnType("text") + .HasColumnName("settings"); + + b.HasKey("Id") + .HasName("pk_oidc_applications"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_oidc_applications_client_id"); + + b.ToTable("oidc_applications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_date"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("Scopes") + .HasColumnType("text") + .HasColumnName("scopes"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("status"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("subject"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_oidc_authorizations"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type") + .HasDatabaseName("ix_oidc_authorizations_application_id_status_subject_type"); + + b.ToTable("oidc_authorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Descriptions") + .HasColumnType("text") + .HasColumnName("descriptions"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("DisplayNames") + .HasColumnType("text") + .HasColumnName("display_names"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("Resources") + .HasColumnType("text") + .HasColumnName("resources"); + + b.HasKey("Id") + .HasName("pk_oidc_scopes"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_oidc_scopes_name"); + + b.ToTable("oidc_scopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("AuthorizationId") + .HasColumnType("uuid") + .HasColumnName("authorization_id"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("concurrency_token"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_date"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.Property("Payload") + .HasColumnType("text") + .HasColumnName("payload"); + + b.Property("Properties") + .HasColumnType("text") + .HasColumnName("properties"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("redemption_date"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("reference_id"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("status"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)") + .HasColumnName("subject"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_oidc_tokens"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasDatabaseName("ix_oidc_tokens_reference_id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type") + .HasDatabaseName("ix_oidc_tokens_application_id_status_subject_type"); + + b.ToTable("oidc_tokens", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("api_key_id"); + + b.Property("ApplicationUserId") + .HasColumnType("uuid") + .HasColumnName("application_user_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("key"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("ApiKeyId") + .HasName("pk_api_keys"); + + b.HasIndex("ApplicationUserId") + .HasDatabaseName("ix_api_keys_application_user_id"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_api_keys_key"); + + b.ToTable("api_keys", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EntityChangesJournal", b => + { + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("EntityLogicalName") + .HasColumnType("text") + .HasColumnName("entity_logical_name"); + + b.Property("DataToken") + .HasColumnType("text") + .HasColumnName("data_token"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_updated"); + + b.Property("LastUpdatedBy") + .HasColumnType("text") + .HasColumnName("last_updated_by"); + + b.Property("NextQueryPageNumber") + .HasColumnType("integer") + .HasColumnName("next_query_page_number"); + + b.Property("NextQueryPageSize") + .HasColumnType("integer") + .HasColumnName("next_query_page_size"); + + b.Property("NextQueryPagingCookie") + .HasColumnType("text") + .HasColumnName("next_query_paging_cookie"); + + b.HasKey("Key", "EntityLogicalName") + .HasName("pk_entity_changes_journals"); + + b.ToTable("entity_changes_journals", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", b => + { + b.Property("EstablishmentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("establishment_id"); + + b.Property("Address3") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("address3") + .UseCollation("case_insensitive"); + + b.Property("County") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("county") + .UseCollation("case_insensitive"); + + b.Property("EstablishmentName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)") + .HasColumnName("establishment_name") + .UseCollation("case_insensitive"); + + b.Property("EstablishmentNumber") + .HasMaxLength(4) + .HasColumnType("character(4)") + .HasColumnName("establishment_number") + .IsFixedLength(); + + b.Property("EstablishmentSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasColumnName("establishment_source_id"); + + b.Property("EstablishmentStatusCode") + .HasColumnType("integer") + .HasColumnName("establishment_status_code"); + + b.Property("EstablishmentStatusName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_status_name"); + + b.Property("EstablishmentTypeCode") + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("establishment_type_code"); + + b.Property("EstablishmentTypeGroupCode") + .HasColumnType("integer") + .HasColumnName("establishment_type_group_code"); + + b.Property("EstablishmentTypeGroupName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("establishment_type_group_name"); + + b.Property("EstablishmentTypeName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("establishment_type_name") + .UseCollation("case_insensitive"); + + b.Property("LaCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("la_code") + .IsFixedLength(); + + b.Property("LaName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("la_name") + .UseCollation("case_insensitive"); + + b.Property("Locality") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("locality") + .UseCollation("case_insensitive"); + + b.Property("Postcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("postcode") + .UseCollation("case_insensitive"); + + b.Property("Street") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("street") + .UseCollation("case_insensitive"); + + b.Property("Town") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("town") + .UseCollation("case_insensitive"); + + b.Property("Urn") + .HasMaxLength(6) + .HasColumnType("integer") + .HasColumnName("urn") + .IsFixedLength(); + + b.HasKey("EstablishmentId") + .HasName("pk_establishments"); + + b.HasIndex("EstablishmentSourceId") + .HasDatabaseName("ix_establishment_establishment_source_id"); + + b.HasIndex("Urn") + .IsUnique() + .HasDatabaseName("ix_establishment_urn"); + + b.HasIndex("LaCode", "EstablishmentNumber") + .HasDatabaseName("ix_establishment_la_code_establishment_number"); + + b.ToTable("establishments", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EstablishmentSource", b => + { + b.Property("EstablishmentSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("establishment_source_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EstablishmentSourceId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name") + .UseCollation("case_insensitive"); + + b.HasKey("EstablishmentSourceId") + .HasName("pk_establishment_sources"); + + b.ToTable("establishment_sources", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Event", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("event_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("EventName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("event_name"); + + b.Property("Inserted") + .HasColumnType("timestamp with time zone") + .HasColumnName("inserted"); + + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.HasKey("EventId") + .HasName("pk_events"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_events_key") + .HasFilter("key is not null"); + + b.HasIndex("Payload") + .HasDatabaseName("ix_events_payload"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Payload"), "gin"); + + b.HasIndex("PersonId", "EventName") + .HasDatabaseName("ix_events_person_id_event_name") + .HasFilter("person_id is not null"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("PersonId", "EventName"), new[] { "Payload" }); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Property("EytsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("EytsAwardedEmailsJobId") + .HasName("pk_eyts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_eyts_awarded_emails_jobs_executed_utc"); + + b.ToTable("eyts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.Property("EytsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("EytsAwardedEmailsJobId", "PersonId") + .HasName("pk_eyts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_eyts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("eyts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Property("InductionCompletedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InductionCompletedEmailsJobId") + .HasName("pk_induction_completed_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_induction_completed_emails_jobs_executed_utc"); + + b.ToTable("induction_completed_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.Property("InductionCompletedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InductionCompletedEmailsJobId", "PersonId") + .HasName("pk_induction_completed_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_induction_completed_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("induction_completed_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InternationalQtsAwardedEmailsJobId") + .HasName("pk_international_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_international_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("international_qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InternationalQtsAwardedEmailsJobId", "PersonId") + .HasName("pk_international_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_international_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("international_qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.JourneyState", b => + { + b.Property("InstanceId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("instance_id"); + + b.Property("Completed") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("user_id"); + + b.HasKey("InstanceId") + .HasName("pk_journey_states"); + + b.ToTable("journey_states", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", b => + { + b.Property("MandatoryQualificationProviderId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("mandatory_qualification_provider_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("MandatoryQualificationProviderId") + .HasName("pk_mandatory_qualification_providers"); + + b.ToTable("mandatory_qualification_providers", (string)null); + + b.HasData( + new + { + MandatoryQualificationProviderId = new Guid("e28ea41d-408d-4c89-90cc-8b9b04ac68f5"), + Name = "University of Birmingham" + }, + new + { + MandatoryQualificationProviderId = new Guid("89f9a1aa-3d68-4985-a4ce-403b6044c18c"), + Name = "University of Leeds" + }, + new + { + MandatoryQualificationProviderId = new Guid("aa5c300e-3b7c-456c-8183-3520b3d55dca"), + Name = "University of Manchester" + }, + new + { + MandatoryQualificationProviderId = new Guid("f417e73e-e2ad-40eb-85e3-55865be7f6be"), + Name = "Mary Hare School / University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("fbf22e04-b274-4c80-aba8-79fb6a7a32ce"), + Name = "University of Edinburgh" + }, + new + { + MandatoryQualificationProviderId = new Guid("26204149-349c-4ad6-9466-bb9b83723eae"), + Name = "Liverpool John Moores University" + }, + new + { + MandatoryQualificationProviderId = new Guid("0c30f666-647c-4ea8-8883-0fc6010b56be"), + Name = "University of Oxford/Oxford Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d0e6d54c-5e90-438a-945d-f97388c2b352"), + Name = "University of Cambridge" + }, + new + { + MandatoryQualificationProviderId = new Guid("aec32252-ef25-452e-a358-34a04e03369c"), + Name = "University of Newcastle-upon-Tyne" + }, + new + { + MandatoryQualificationProviderId = new Guid("d9ee7054-7fde-4cfd-9a5e-4b99511d1b3d"), + Name = "University of Plymouth" + }, + new + { + MandatoryQualificationProviderId = new Guid("707d58ca-1953-413b-9a46-41e9b0be885e"), + Name = "University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("3fc648a7-18e4-49e7-8a4b-1612616b72d5"), + Name = "University of London" + }, + new + { + MandatoryQualificationProviderId = new Guid("374dceb8-8224-45b8-b7dc-a6b0282b1065"), + Name = "Bristol Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d4fc958b-21de-47ec-9f03-36ae237a1b11"), + Name = "University College, Swansea" + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.NameSynonyms", b => + { + b.Property("NameSynonymsId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("name_synonyms_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NameSynonymsId")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .UseCollation("case_insensitive"); + + b.Property("Synonyms") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("synonyms") + .UseCollation("case_insensitive"); + + b.HasKey("NameSynonymsId") + .HasName("pk_name_synonyms"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_name_synonyms_name"); + + b.ToTable("name_synonyms", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", b => + { + b.Property("Subject") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("subject"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("FirstOneLoginSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_one_login_sign_in"); + + b.Property("FirstSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_sign_in"); + + b.Property("LastOneLoginSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_one_login_sign_in"); + + b.Property("LastSignIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sign_in"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("VerificationRoute") + .HasColumnType("integer") + .HasColumnName("verification_route"); + + b.Property("VerifiedDatesOfBirth") + .HasColumnType("jsonb") + .HasColumnName("verified_dates_of_birth"); + + b.Property("VerifiedNames") + .HasColumnType("jsonb") + .HasColumnName("verified_names"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_on"); + + b.HasKey("Subject") + .HasName("pk_one_login_users"); + + b.ToTable("one_login_users", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtContactId") + .HasColumnType("uuid") + .HasColumnName("dqt_contact_id"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_first_name") + .UseCollation("case_insensitive"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_last_name") + .UseCollation("case_insensitive"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtMiddleName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_middle_name") + .UseCollation("case_insensitive"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("EmailAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("email_address") + .UseCollation("case_insensitive"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("first_name") + .UseCollation("case_insensitive"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("last_name") + .UseCollation("case_insensitive"); + + b.Property("MiddleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("middle_name") + .UseCollation("case_insensitive"); + + b.Property("NationalInsuranceNumber") + .HasMaxLength(9) + .HasColumnType("character(9)") + .HasColumnName("national_insurance_number") + .IsFixedLength(); + + b.Property("Trn") + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("PersonId") + .HasName("pk_persons"); + + b.HasIndex("DqtContactId") + .IsUnique() + .HasDatabaseName("ix_persons_dqt_contact_id") + .HasFilter("dqt_contact_id is not null"); + + b.HasIndex("Trn") + .IsUnique() + .HasDatabaseName("ix_persons_trn") + .HasFilter("trn is not null"); + + b.ToTable("persons", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonEmployment", b => + { + b.Property("PersonEmploymentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("person_employment_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("EmploymentType") + .HasColumnType("integer") + .HasColumnName("employment_type"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("EstablishmentId") + .HasColumnType("uuid") + .HasColumnName("establishment_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("PersonEmploymentId") + .HasName("pk_person_employments"); + + b.HasIndex("EstablishmentId") + .HasDatabaseName("ix_person_employments_establishment_id"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_employments_person_id"); + + b.ToTable("person_employments", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonSearchAttribute", b => + { + b.Property("PersonSearchAttributeId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("person_search_attribute_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersonSearchAttributeId")); + + b.Property("AttributeKey") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("attribute_key") + .UseCollation("case_insensitive"); + + b.Property("AttributeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("attribute_type") + .UseCollation("case_insensitive"); + + b.Property("AttributeValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("attribute_value") + .UseCollation("case_insensitive"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("tags"); + + b.HasKey("PersonSearchAttributeId") + .HasName("pk_person_search_attributes"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_search_attributes_person_id"); + + b.HasIndex("AttributeType", "AttributeValue") + .HasDatabaseName("ix_person_search_attributes_attribute_type_and_value"); + + b.ToTable("person_search_attributes", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Property("QtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("QtsAwardedEmailsJobId") + .HasName("pk_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.Property("QtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("QtsAwardedEmailsJobId", "PersonId") + .HasName("pk_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.Property("QualificationId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qualification_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtQualificationId") + .HasColumnType("uuid") + .HasColumnName("dqt_qualification_id"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("QualificationType") + .HasColumnType("integer") + .HasColumnName("qualification_type"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("QualificationId") + .HasName("pk_qualifications"); + + b.HasIndex("DqtQualificationId") + .IsUnique() + .HasDatabaseName("ix_qualifications_dqt_qualification_id") + .HasFilter("dqt_qualification_id is not null"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_qualifications_person_id"); + + b.ToTable("qualifications", (string)null); + + b.HasDiscriminator("QualificationType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", b => + { + b.Property("TpsCsvExtractId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Filename") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("filename"); + + b.HasKey("TpsCsvExtractId") + .HasName("pk_tps_csv_extracts"); + + b.ToTable("tps_csv_extracts", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractItem", b => + { + b.Property("TpsCsvExtractItemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_item_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("DateOfDeath") + .HasColumnType("date") + .HasColumnName("date_of_death"); + + b.Property("EmploymentEndDate") + .HasColumnType("date") + .HasColumnName("employment_end_date"); + + b.Property("EmploymentStartDate") + .HasColumnType("date") + .HasColumnName("employment_start_date"); + + b.Property("EmploymentType") + .HasColumnType("integer") + .HasColumnName("employment_type"); + + b.Property("EstablishmentEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_email_address"); + + b.Property("EstablishmentNumber") + .HasMaxLength(4) + .HasColumnType("character(4)") + .HasColumnName("establishment_number") + .IsFixedLength(); + + b.Property("EstablishmentPostcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("establishment_postcode"); + + b.Property("ExtractDate") + .HasColumnType("date") + .HasColumnName("extract_date"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("gender"); + + b.Property("LocalAuthorityCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character(3)") + .HasColumnName("local_authority_code") + .IsFixedLength(); + + b.Property("MemberEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_email_address"); + + b.Property("MemberId") + .HasColumnType("integer") + .HasColumnName("member_id"); + + b.Property("MemberPostcode") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("member_postcode"); + + b.Property("NationalInsuranceNumber") + .IsRequired() + .HasMaxLength(9) + .HasColumnType("character(9)") + .HasColumnName("national_insurance_number") + .IsFixedLength(); + + b.Property("Result") + .HasColumnType("integer") + .HasColumnName("result"); + + b.Property("TpsCsvExtractId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("TpsCsvExtractLoadItemId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_load_item_id"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.Property("WithdrawlIndicator") + .HasMaxLength(1) + .HasColumnType("character(1)") + .HasColumnName("withdrawl_indicator") + .IsFixedLength(); + + b.HasKey("TpsCsvExtractItemId") + .HasName("pk_tps_csv_extract_items"); + + b.HasIndex("TpsCsvExtractId") + .HasDatabaseName("ix_tps_csv_extract_items_tps_csv_extract_id"); + + b.HasIndex("TpsCsvExtractLoadItemId") + .HasDatabaseName("ix_tps_csv_extract_items_tps_csv_extract_load_item_id"); + + b.HasIndex("Trn") + .HasDatabaseName("ix_tps_csv_extract_items_trn"); + + b.HasIndex("LocalAuthorityCode", "EstablishmentNumber") + .HasDatabaseName("ix_tps_csv_extract_items_la_code_establishment_number"); + + b.ToTable("tps_csv_extract_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", b => + { + b.Property("TpsCsvExtractLoadItemId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_load_item_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("DateOfBirth") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("date_of_birth"); + + b.Property("DateOfDeath") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("date_of_death"); + + b.Property("EmploymentEndDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("employment_end_date"); + + b.Property("EmploymentStartDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("employment_start_date"); + + b.Property("Errors") + .HasColumnType("integer") + .HasColumnName("errors"); + + b.Property("EstablishmentEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_email_address"); + + b.Property("EstablishmentNumber") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_number"); + + b.Property("EstablishmentPostcode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("establishment_postcode"); + + b.Property("ExtractDate") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("extract_date"); + + b.Property("FullOrPartTimeIndicator") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("full_or_part_time_indicator"); + + b.Property("Gender") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("gender"); + + b.Property("LocalAuthorityCode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("local_authority_code"); + + b.Property("MemberEmailAddress") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_email_address"); + + b.Property("MemberId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_id"); + + b.Property("MemberPostcode") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("member_postcode"); + + b.Property("NationalInsuranceNumber") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("national_insurance_number"); + + b.Property("TpsCsvExtractId") + .HasColumnType("uuid") + .HasColumnName("tps_csv_extract_id"); + + b.Property("Trn") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("trn"); + + b.Property("WithdrawlIndicator") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("withdrawl_indicator"); + + b.HasKey("TpsCsvExtractLoadItemId") + .HasName("pk_tps_csv_extract_load_items"); + + b.ToTable("tps_csv_extract_load_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TrnRequest", b => + { + b.Property("TrnRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("trn_request_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TrnRequestId")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_id"); + + b.Property("IdentityUserId") + .HasColumnType("uuid") + .HasColumnName("identity_user_id"); + + b.Property("LinkedToIdentity") + .HasColumnType("boolean") + .HasColumnName("linked_to_identity"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("request_id"); + + b.Property("TeacherId") + .HasColumnType("uuid") + .HasColumnName("teacher_id"); + + b.Property("TrnToken") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("trn_token"); + + b.HasKey("TrnRequestId") + .HasName("pk_trn_requests"); + + b.HasIndex("ClientId", "RequestId") + .IsUnique() + .HasDatabaseName("ix_trn_requests_client_id_request_id"); + + b.ToTable("trn_requests", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UserType") + .HasColumnType("integer") + .HasColumnName("user_type"); + + b.HasKey("UserId") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + + b.HasDiscriminator("UserType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification"); + + b.Property("DqtMqEstablishmentId") + .HasColumnType("uuid") + .HasColumnName("dqt_mq_establishment_id"); + + b.Property("DqtSpecialismId") + .HasColumnType("uuid") + .HasColumnName("dqt_specialism_id"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("ProviderId") + .HasColumnType("uuid") + .HasColumnName("mq_provider_id"); + + b.Property("Specialism") + .HasColumnType("integer") + .HasColumnName("mq_specialism"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("mq_status"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("ApiRoles") + .HasColumnType("varchar[]") + .HasColumnName("api_roles"); + + b.Property("ClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("client_secret"); + + b.Property("IsOidcClient") + .HasColumnType("boolean") + .HasColumnName("is_oidc_client"); + + b.Property("OneLoginAuthenticationSchemeName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_authentication_scheme_name"); + + b.Property("OneLoginClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_client_id"); + + b.Property("OneLoginPostLogoutRedirectUriPath") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("one_login_post_logout_redirect_uri_path"); + + b.Property("OneLoginPrivateKeyPem") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("one_login_private_key_pem"); + + b.Property("OneLoginRedirectUriPath") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("one_login_redirect_uri_path"); + + b.Property>("PostLogoutRedirectUris") + .HasColumnType("varchar[]") + .HasColumnName("post_logout_redirect_uris"); + + b.Property>("RedirectUris") + .HasColumnType("varchar[]") + .HasColumnName("redirect_uris"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_users_client_id") + .HasFilter("client_id is not null"); + + b.HasIndex("OneLoginAuthenticationSchemeName") + .IsUnique() + .HasDatabaseName("ix_users_one_login_authentication_scheme_name") + .HasFilter("one_login_authentication_scheme_name is not null"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.SystemUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.HasDiscriminator().HasValue(3); + + b.HasData( + new + { + UserId = new Guid("a81394d1-a498-46d8-af3e-e077596ab303"), + Active = true, + Name = "System", + UserType = 0 + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.User", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("AzureAdUserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("azure_ad_user_id"); + + b.Property("DqtUserId") + .HasColumnType("uuid") + .HasColumnName("dqt_user_id"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .UseCollation("case_insensitive"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("varchar[]") + .HasColumnName("roles"); + + b.HasIndex("AzureAdUserId") + .IsUnique() + .HasDatabaseName("ix_users_azure_ad_user_id"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId") + .HasConstraintName("fk_oidc_authorizations_oidc_applications_application_id"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId") + .HasConstraintName("fk_oidc_tokens_oidc_applications_application_id"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId") + .HasConstraintName("fk_oidc_tokens_oidc_authorizations_authorization_id"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", "ApplicationUser") + .WithMany("ApiKeys") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_key_application_user"); + + b.Navigation("ApplicationUser"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.EstablishmentSource", null) + .WithMany() + .HasForeignKey("EstablishmentSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_establishments_establishment_source_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", "EytsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("EytsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_eyts_awarded_emails_job_items_eyts_awarded_emails_jobs_eyts"); + + b.Navigation("EytsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", "InductionCompletedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InductionCompletedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_induction_completed_emails_job_items_induction_completed_em"); + + b.Navigation("InductionCompletedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", "InternationalQtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InternationalQtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_international_qts_awarded_emails_job_items_international_qt"); + + b.Navigation("InternationalQtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", "Person") + .WithOne() + .HasForeignKey("TeachingRecordSystem.Core.DataStore.Postgres.Models.OneLoginUser", "PersonId") + .HasConstraintName("fk_one_login_users_persons_person_id"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.PersonEmployment", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment", null) + .WithMany() + .HasForeignKey("EstablishmentId") + .HasConstraintName("fk_person_employments_establishment_id"); + + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_person_employments_person_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", "QtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("QtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qts_awarded_emails_job_items_qts_awarded_emails_jobs_qts_aw"); + + b.Navigation("QtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qualifications_person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", null) + .WithMany() + .HasForeignKey("TpsCsvExtractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_items_tps_csv_extract_id"); + + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", null) + .WithMany() + .HasForeignKey("TpsCsvExtractLoadItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_items_tps_csv_extract_load_item_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtractLoadItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.TpsCsvExtract", null) + .WithMany() + .HasForeignKey("TpsCsvExtractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tps_csv_extract_load_items_tps_csv_extract_id"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .HasConstraintName("fk_qualifications_mandatory_qualification_provider"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.Navigation("ApiKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.cs new file mode 100644 index 000000000..0a7e02783 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240411145144_VerifiedDetails.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + /// + public partial class VerifiedDetails : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "verified_dates_of_birth", + table: "one_login_users", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "verified_names", + table: "one_login_users", + type: "jsonb", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "verified_dates_of_birth", + table: "one_login_users"); + + migrationBuilder.DropColumn( + name: "verified_names", + table: "one_login_users"); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs index 08309eda3..3ddec4749 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs @@ -969,6 +969,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("person_id"); + b.Property("VerificationRoute") + .HasColumnType("integer") + .HasColumnName("verification_route"); + + b.Property("VerifiedDatesOfBirth") + .HasColumnType("jsonb") + .HasColumnName("verified_dates_of_birth"); + + b.Property("VerifiedNames") + .HasColumnType("jsonb") + .HasColumnName("verified_names"); + + b.Property("VerifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_on"); + b.HasKey("Subject") .HasName("pk_one_login_users"); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/OneLoginUser.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/OneLoginUser.cs index 968176c5d..0b1fe8cb8 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/OneLoginUser.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/OneLoginUser.cs @@ -10,4 +10,8 @@ public class OneLoginUser public DateTime? LastSignIn { get; set; } public Guid? PersonId { get; set; } public Person? Person { get; } + public DateTime? VerifiedOn { get; set; } + public OneLoginUserVerificationRoute? VerificationRoute { get; set; } + public string[][]? VerifiedNames { get; set; } + public DateOnly[]? VerifiedDatesOfBirth { get; set; } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/OneLoginUserVerificationRoute.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/OneLoginUserVerificationRoute.cs new file mode 100644 index 000000000..4c53a81f9 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/OneLoginUserVerificationRoute.cs @@ -0,0 +1,7 @@ +namespace TeachingRecordSystem.Core; + +public enum OneLoginUserVerificationRoute +{ + OneLogin = 1, + Support = 2 +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/Infrastructure/Security/FakeOneLoginHandler.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/Infrastructure/Security/FakeOneLoginHandler.cs index 71cf9325b..1a30bc54d 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/Infrastructure/Security/FakeOneLoginHandler.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/Infrastructure/Security/FakeOneLoginHandler.cs @@ -1,12 +1,15 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; +using GovUk.OneLogin.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using TeachingRecordSystem.AuthorizeAccess.Infrastructure.FormFlow; using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Security; +using TeachingRecordSystem.FormFlow; namespace TeachingRecordSystem.AuthorizeAccess.EndToEndTests.Infrastructure.Security; -public class FakeOneLoginHandler(OneLoginCurrentUserProvider currentUserProvider) : IAuthenticationHandler, IAuthenticationSignOutHandler +public class FakeOneLoginHandler(OneLoginCurrentUserProvider currentUserProvider, SignInJourneyHelper signInJourneyHelper) : IAuthenticationHandler, IAuthenticationSignOutHandler { private HttpContext? _context; @@ -22,6 +25,30 @@ public async Task ChallengeAsync(AuthenticationProperties? properties) _ = _context ?? throw new InvalidOperationException("Not initialized."); var user = currentUserProvider.CurrentUser ?? throw new InvalidOperationException("No current user set."); + if (properties is null || !properties.TryGetVectorOfTrust(out var vtr)) + { + throw new InvalidOperationException("No vtr set."); + } + + if (vtr == SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr && user.CoreIdentityVc is null) + { + // Simulate an 'access_denied' error for failing ID verification + + if (!properties.Items.TryGetValue(FormFlowJourneySignInHandler.PropertyKeys.JourneyInstanceId, out var serializedInstanceId) || serializedInstanceId is null) + { + throw new InvalidOperationException("No JourneyInstanceId set."); + } + + var journeyInstanceId = JourneyInstanceId.Deserialize(serializedInstanceId); + + var journeyInstance = (await signInJourneyHelper.UserInstanceStateProvider.GetSignInJourneyInstanceAsync(_context, journeyInstanceId))!; + + var result = await signInJourneyHelper.OnUserVerificationWithOneLoginFailed(journeyInstance); + await result.ExecuteAsync(_context); + + return; + } + var claims = new List() { new("sub", user.sub), diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/OidcTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/OidcTests.cs index b20823130..024b64936 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/OidcTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/OidcTests.cs @@ -8,7 +8,7 @@ public class OidcTests(HostFixture hostFixture) : TestBase(hostFixture) public async Task SignInAndOut() { var person = await TestData.CreatePerson(x => x.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); + var oneLoginUser = await TestData.CreateOneLoginUser(person); var coreIdentityVc = TestData.CreateOneLoginCoreIdentityVc(person.FirstName, person.LastName, person.DateOfBirth); SetCurrentOneLoginUser(OneLoginUserInfo.Create(oneLoginUser.Subject, oneLoginUser.Email, coreIdentityVc)); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs index b7867433b..3885a1649 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.EndToEndTests/SignInTests.cs @@ -183,7 +183,7 @@ public async Task SignIn_UnknownVerifiedUserWithNeitherNinoNorTrn_DoesNotMatch() public async Task SignIn_KnownUser() { var person = await TestData.CreatePerson(x => x.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); + var oneLoginUser = await TestData.CreateOneLoginUser(person); var coreIdentityVc = TestData.CreateOneLoginCoreIdentityVc(person.FirstName, person.LastName, person.DateOfBirth); SetCurrentOneLoginUser(OneLoginUserInfo.Create(oneLoginUser.Subject, oneLoginUser.Email, coreIdentityVc)); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs index 9bf1f0189..9b5eaacf9 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/CheckAnswersTests.cs @@ -27,7 +27,7 @@ public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -46,7 +46,9 @@ public async Task Get_NationalInsuranceNumberNotSpecified_RedirectsToNationalIns var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.NationalInsuranceNumber is null); @@ -68,7 +70,9 @@ public async Task Get_TrnNotSpecified_RedirectsToTrnPage() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.NationalInsuranceNumber is null); @@ -92,15 +96,9 @@ public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -120,7 +118,9 @@ public async Task Get_ValidRequest_RendersExpectedContent() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var nationalInsuranceNumber = TestData.GenerateNationalInsuranceNumber(); @@ -166,7 +166,7 @@ public async Task Post_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -185,7 +185,9 @@ public async Task Post_NationalInsuranceNumberNotSpecified_RedirectsToNationalIn var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.NationalInsuranceNumber is null); @@ -207,7 +209,9 @@ public async Task Post_TrnNotSpecified_RedirectsToTrnPage() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.NationalInsuranceNumber is null); @@ -231,15 +235,9 @@ public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -253,13 +251,15 @@ public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() } [Fact] - public async Task Post_ValidRequests_RedirectsToNotFoundPage() + public async Task Post_ValidRequest_RedirectsToNotFoundPage() { // Arrange var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var nationalInsuranceNumber = TestData.GenerateNationalInsuranceNumber(); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/ConnectTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/ConnectTests.cs index 48e913e6c..79481720f 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/ConnectTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/ConnectTests.cs @@ -25,7 +25,7 @@ public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/connect?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -45,15 +45,9 @@ public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/connect?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -73,7 +67,9 @@ public async Task Get_ValidRequest_RendersExpectedContent() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/connect?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -107,7 +103,7 @@ public async Task Post_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/connect?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -127,15 +123,9 @@ public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/connect?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -155,7 +145,9 @@ public async Task Post_ValidRequest_RedirectsToStartOfMatchingJourney() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/connect?{journeyInstance.GetUniqueIdQueryParameter()}"); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/FoundTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/FoundTests.cs index 8ff1de224..f5c00ca93 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/FoundTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/FoundTests.cs @@ -5,13 +5,15 @@ namespace TeachingRecordSystem.AuthorizeAccess.Tests.PageTests; public class FoundTests(HostFixture hostFixture) : TestBase(hostFixture) { [Fact] - public async Task Get_NotAuthenticated_RedirectsToStartOfMatchingJourney() + public async Task Get_NotFullyAuthenticated_RedirectsToStartOfMatchingJourney() { // Arrange var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.AuthenticationTicket is null); @@ -33,15 +35,9 @@ public async Task Get_ValidRequest_RendersExpectedContent() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/found?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -60,7 +56,9 @@ public async Task Post_NotAuthenticated_RedirectsToStartOfMatchingJourney() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.AuthenticationTicket is null); @@ -82,15 +80,9 @@ public async Task Post_ValidRequest_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/found?{journeyInstance.GetUniqueIdQueryParameter()}"); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs index a88e4e818..3dca5c216 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs @@ -25,7 +25,7 @@ public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -45,15 +45,9 @@ public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -75,7 +69,9 @@ public async Task Get_ValidRequest_RendersExpectedContent(bool haveExistingValue var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var existingNationalInsuranceNumber = haveExistingValueInState ? Faker.Identification.UkNationalInsuranceNumber() : null; @@ -125,11 +121,11 @@ public async Task Post_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); - - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder @@ -152,19 +148,14 @@ public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var nationalInsuranceNumber = person.NationalInsuranceNumber!; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var person = await TestData.CreatePerson(b => b.WithTrn()); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var nationalInsuranceNumber = person.NationalInsuranceNumber!; + var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder @@ -188,16 +179,9 @@ public async Task Post_HaveNationalInsuranceNumberNotAnswered_RendersError() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -221,19 +205,13 @@ public async Task Post_EmptyNationalInsuranceNumber_RendersError() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var nationalInsuranceNumber = ""; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var nationalInsuranceNumber = ""; + var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder @@ -257,19 +235,13 @@ public async Task Post_InvalidNationalInsuranceNumber_RendersError() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var nationalInsuranceNumber = "xxx"; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var nationalInsuranceNumber = "xxx"; + var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder @@ -287,22 +259,15 @@ public async Task Post_InvalidNationalInsuranceNumber_RendersError() } [Fact] - public async Task Post_NoNationalInsuranceNumberSpecified_UpdatesStateAndRedirectsToTrnPage() + public async Task Post_HaveNationalInsuranceNumberNotSpecified_UpdatesStateAndRedirectsToTrnPage() { // Arrange var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -333,19 +298,13 @@ public async Task Post_ValidNationalInsuranceNumberButLookupFailed_UpdatesStateA var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder @@ -377,18 +336,15 @@ public async Task Post_ValidNationalInsuranceNumberAndLookupSucceeded_UpdatesSta var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var nationalInsuranceNumber = person.NationalInsuranceNumber!; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser( + personId: null, + verifiedInfo: ([person.FirstName, person.LastName], person.DateOfBirth)); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var nationalInsuranceNumber = person.NationalInsuranceNumber!; + var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotFoundTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotFoundTests.cs index 82bf7ccd0..98d5c8de0 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotFoundTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotFoundTests.cs @@ -27,7 +27,9 @@ public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: false); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -46,16 +48,10 @@ public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); + var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var oneLoginUser = await TestData.CreateOneLoginUser(person); - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -75,7 +71,9 @@ public async Task Get_NationalInsuranceNumberNotSpecified_RedirectsToNationalIns var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.NationalInsuranceNumber is null); @@ -97,7 +95,9 @@ public async Task Get_TrnNotSpecified_RedirectsToTrnPage() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.NationalInsuranceNumber is null); @@ -120,7 +120,9 @@ public async Task Get_ValidRequest_RendersExpectedContent() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); await journeyInstance.UpdateStateAsync(async state => diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotVerifiedTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotVerifiedTests.cs index 022ae2c25..d3b5f1e2a 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotVerifiedTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotVerifiedTests.cs @@ -18,6 +18,29 @@ public async Task Get_NotAuthenticatedWithOneLogin_ReturnsBadRequest() Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); } + [Fact] + public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() + { + // Arrange + var state = CreateNewState(); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/not-verified?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"{state.RedirectUri}?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + [Fact] public async Task Get_VerifiedWithOneLogin_ReturnsBadRequest() { @@ -25,7 +48,9 @@ public async Task Get_VerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: true); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/not-verified?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -44,7 +69,9 @@ public async Task Get_ValidRequest_ReturnsExpectedContent() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: false); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/not-verified?{journeyInstance.GetUniqueIdQueryParameter()}"); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/TrnTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/TrnTests.cs index d18a5e188..3ca7b7c40 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/TrnTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/TrnTests.cs @@ -27,7 +27,7 @@ public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -47,15 +47,9 @@ public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn()); - var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Get, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}"); @@ -75,7 +69,9 @@ public async Task Get_NationalInsuranceNumberNotSpecified_RedirectsToNationalIns var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); Debug.Assert(state.NationalInsuranceNumber is null); @@ -99,7 +95,9 @@ public async Task Get_ValidRequest_RendersExpectedContent(bool haveExistingValue var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var existingTrn = haveExistingValueInState ? await TestData.GenerateTrn() : null; @@ -160,7 +158,7 @@ public async Task Post_NotVerifiedWithOneLogin_ReturnsBadRequest() var trn = await TestData.GenerateTrn(); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, createCoreIdentityVc: false); + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, createCoreIdentityVc: false); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -186,19 +184,14 @@ public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var trn = person.Trn!; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: person.PersonId); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var person = await TestData.CreatePerson(b => b.WithTrn()); + var oneLoginUser = await TestData.CreateOneLoginUser(person); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var trn = person.Trn!; + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder @@ -223,19 +216,13 @@ public async Task Post_NationalInsuranceNumberNotSpecified_RedirectsToNationalIn var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var trn = person.Trn!; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var trn = await TestData.GenerateTrn(); + Debug.Assert(state.NationalInsuranceNumber is null); var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -262,16 +249,9 @@ public async Task Post_HaveTrnNotAnswered_RendersError() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, TestData.GenerateNationalInsuranceNumber())); @@ -297,19 +277,13 @@ public async Task Post_EmptyTrn_RendersError() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var trn = ""; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var trn = ""; + await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, TestData.GenerateNationalInsuranceNumber())); var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -335,19 +309,13 @@ public async Task Post_InvalidTrn_RendersError() var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var trn = "xxx"; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var trn = "xxx"; + await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, TestData.GenerateNationalInsuranceNumber())); var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -373,19 +341,13 @@ public async Task Post_NoTrnSpecified_UpdatesStateAndRedirectsToCheckAnswersPage var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var trn = await TestData.GenerateTrn(); - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var trn = await TestData.GenerateTrn(); + await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, TestData.GenerateNationalInsuranceNumber())); var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -417,19 +379,13 @@ public async Task Post_ValidTrnButLookupFailed_UpdatesStateAndRedirectsToCheckAn var state = CreateNewState(); var journeyInstance = await CreateJourneyInstance(state); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var trn = await TestData.GenerateTrn(); - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser(verified: true); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var trn = await TestData.GenerateTrn(); + await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, TestData.GenerateNationalInsuranceNumber())); var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") @@ -463,18 +419,15 @@ public async Task Post_ValidTrnAndLookupSucceeded_UpdatesStateUpdatesOneLoginUse var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var trn = person.Trn!; - var oneLoginUser = await TestData.CreateOneLoginUser(personId: null); - - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, - sub: oneLoginUser.Subject, - email: oneLoginUser.Email, - firstName: person.FirstName, - lastName: person.LastName, - dateOfBirth: person.DateOfBirth); + var oneLoginUser = await TestData.CreateOneLoginUser( + personId: null, + verifiedInfo: ([person.FirstName, person.LastName], person.DateOfBirth)); + + var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, oneLoginUser); await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + var trn = person.Trn!; + await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, TestData.GenerateNationalInsuranceNumber())); var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs index 042b5bf63..c5b963c91 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Security.Claims; using GovUk.OneLogin.AspNetCore; using Microsoft.AspNetCore.Http.HttpResults; @@ -17,16 +18,16 @@ public Task OnSignedInWithOneLogin_AuthenticationOnly_UserAlreadyExistsAndTeachi var helper = CreateHelper(dbContext); var person = await TestData.CreatePerson(b => b.WithTrn(true)); - var user = await TestData.CreateOneLoginUser(personId: person.PersonId); + var user = await TestData.CreateOneLoginUser(person); Clock.Advance(); var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, sub: user.Subject); + var authenticationTicket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, sub: user.Subject); // Act - var result = await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var result = await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); // Assert Assert.NotNull(state.AuthenticationTicket); @@ -54,10 +55,10 @@ public Task OnSignedInWithOneLogin_AuthenticationOnly_UserAlreadyExistsButTeachi var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, sub: user.Subject); + var authenticationTicket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, sub: user.Subject); // Act - var result = await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var result = await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); // Assert Assert.NotNull(state.OneLoginAuthenticationTicket); @@ -84,10 +85,10 @@ public Task OnSignedInWithOneLogin_AuthenticationOnly_UserDoesNotExist_RequestsI var journeyInstance = await CreateJourneyInstance(state); var subject = TestData.CreateOneLoginUserSubject(); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, sub: subject); + var authenticationTicket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationOnlyVtr, sub: subject); // Act - var result = await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var result = await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); // Assert Assert.NotNull(state.OneLoginAuthenticationTicket); @@ -117,13 +118,15 @@ public Task OnSignedInWithOneLogin_AuthenticationAndVerification_VerificationFai var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket( - vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, + var authenticationTicket = CreateOneLoginAuthenticationTicket( + vtr: SignInJourneyHelper.AuthenticationOnlyVtr, sub: user.Subject, createCoreIdentityVc: false); + var onAuthenticatedResult = await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); + Debug.Assert(onAuthenticatedResult is ChallengeHttpResult); // Act - var result = await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var result = await helper.OnUserVerificationWithOneLoginFailed(journeyInstance); // Assert Assert.NotNull(state.OneLoginAuthenticationTicket); @@ -149,16 +152,33 @@ public Task OnSignedInWithOneLogin_AuthenticationAndVerification_VerificationSuc var user = await TestData.CreateOneLoginUser(personId: null); Clock.Advance(); + var firstName = Faker.Name.First(); + var lastName = Faker.Name.Last(); + var dateOfBirth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()); + var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var ticket = CreateOneLoginAuthenticationTicket( + var authenticationTicket = CreateOneLoginAuthenticationTicket( + vtr: SignInJourneyHelper.AuthenticationOnlyVtr, + sub: user.Subject, + firstName: firstName, + lastName: lastName, + dateOfBirth: dateOfBirth, + createCoreIdentityVc: false); + var onAuthenticatedResult = await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); + Debug.Assert(onAuthenticatedResult is ChallengeHttpResult); + + var verifiedTicket = CreateOneLoginAuthenticationTicket( vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, sub: user.Subject, + firstName: firstName, + lastName: lastName, + dateOfBirth: dateOfBirth, createCoreIdentityVc: true); // Act - var result = await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var result = await helper.OnSignedInWithOneLogin(journeyInstance, verifiedTicket); // Assert Assert.NotNull(state.OneLoginAuthenticationTicket); @@ -169,6 +189,12 @@ public Task OnSignedInWithOneLogin_AuthenticationAndVerification_VerificationSuc Assert.Null(user.FirstSignIn); Assert.Equal(Clock.UtcNow, user.LastOneLoginSignIn); Assert.NotEqual(Clock.UtcNow, user.LastSignIn); + Assert.Equal(Clock.UtcNow, user.VerifiedOn); + Assert.Equal(OneLoginUserVerificationRoute.OneLogin, user.VerificationRoute); + Assert.NotNull(user.VerifiedNames); + Assert.Collection(user.VerifiedNames, names => Assert.Collection(names, n => Assert.Equal(firstName, n), n => Assert.Equal(lastName, n))); + Assert.NotNull(user.VerifiedDatesOfBirth); + Assert.Collection(user.VerifiedDatesOfBirth, dob => Assert.Equal(dateOfBirth, dob)); var redirectResult = Assert.IsType(result); Assert.Equal($"/Connect?{journeyInstance.GetUniqueIdQueryParameter()}", redirectResult.Url); @@ -182,12 +208,20 @@ public Task TryMatchToTeachingRecord_MatchesZeroResults_ReturnsFalseAndDoesNotSe var personSearchServiceMock = new Mock(); var helper = CreateHelper(dbContext, personSearchServiceMock.Object); + var verifiedFirstName = Faker.Name.Last(); + var verifiedLastName = Faker.Name.Last(); + var verifiedDateOfBirth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()); + var user = await TestData.CreateOneLoginUser(personId: null, verifiedInfo: ([verifiedFirstName, verifiedLastName], verifiedDateOfBirth)); + Clock.Advance(); + var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var subject = TestData.CreateOneLoginUserSubject(); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, sub: subject, createCoreIdentityVc: true); - await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var authenticationTicket = CreateOneLoginAuthenticationTicket( + vtr: SignInJourneyHelper.AuthenticationOnlyVtr, + sub: user.Subject, + createCoreIdentityVc: false); + await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, Faker.Identification.UkNationalInsuranceNumber())); @@ -205,7 +239,7 @@ public Task TryMatchToTeachingRecord_MatchesZeroResults_ReturnsFalseAndDoesNotSe // Assert Assert.False(result); - var user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == subject); + user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == user.Subject); Assert.Null(user.PersonId); Assert.Null(state.AuthenticationTicket); @@ -219,15 +253,23 @@ public Task TryMatchToTeachingRecord_MatchesMultipleResults_ReturnsFalseAndDoesN var personSearchServiceMock = new Mock(); var helper = CreateHelper(dbContext, personSearchServiceMock.Object); - var person1 = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var person2 = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var firstName = Faker.Name.Last(); + var lastName = Faker.Name.Last(); + var dateOfBirth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()); + var person1 = await TestData.CreatePerson(b => b.WithTrn().WithFirstName(firstName).WithLastName(lastName).WithDateOfBirth(dateOfBirth).WithNationalInsuranceNumber()); + var person2 = await TestData.CreatePerson(b => b.WithTrn().WithFirstName(firstName).WithLastName(lastName).WithDateOfBirth(dateOfBirth).WithNationalInsuranceNumber()); + + var user = await TestData.CreateOneLoginUser(personId: null, verifiedInfo: ([firstName, lastName], dateOfBirth)); + Clock.Advance(); var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var subject = TestData.CreateOneLoginUserSubject(); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, sub: subject, createCoreIdentityVc: true); - await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var authenticationTicket = CreateOneLoginAuthenticationTicket( + vtr: SignInJourneyHelper.AuthenticationOnlyVtr, + sub: user.Subject, + createCoreIdentityVc: false); + await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, person1.NationalInsuranceNumber)); @@ -267,7 +309,7 @@ public Task TryMatchToTeachingRecord_MatchesMultipleResults_ReturnsFalseAndDoesN // Assert Assert.False(result); - var user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == subject); + user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == user.Subject); Assert.Null(user.PersonId); Assert.Null(state.AuthenticationTicket); @@ -281,14 +323,22 @@ public Task TryMatchToTeachingRecord_MatchesSingleResultWithoutTrn_ReturnsFalseA var personSearchServiceMock = new Mock(); var helper = CreateHelper(dbContext, personSearchServiceMock.Object); - var person = await TestData.CreatePerson(b => b.WithTrn(false).WithNationalInsuranceNumber()); + var firstName = Faker.Name.Last(); + var lastName = Faker.Name.Last(); + var dateOfBirth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()); + var person = await TestData.CreatePerson(b => b.WithTrn(false).WithFirstName(firstName).WithLastName(lastName).WithDateOfBirth(dateOfBirth).WithNationalInsuranceNumber()); + + var user = await TestData.CreateOneLoginUser(personId: null, verifiedInfo: ([firstName, lastName], dateOfBirth)); + Clock.Advance(); var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var subject = TestData.CreateOneLoginUserSubject(); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, sub: subject, createCoreIdentityVc: true); - await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var authenticationTicket = CreateOneLoginAuthenticationTicket( + vtr: SignInJourneyHelper.AuthenticationOnlyVtr, + sub: user.Subject, + createCoreIdentityVc: false); + await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, person.NationalInsuranceNumber)); @@ -318,7 +368,7 @@ public Task TryMatchToTeachingRecord_MatchesSingleResultWithoutTrn_ReturnsFalseA // Assert Assert.False(result); - var user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == subject); + user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == user.Subject); Assert.Null(user.PersonId); Assert.Null(state.AuthenticationTicket); @@ -332,14 +382,22 @@ public Task TryMatchToTeachingRecord_MatchesSingleResult_ReturnsTrueAndUpdatesOn var personSearchServiceMock = new Mock(); var helper = CreateHelper(dbContext, personSearchServiceMock.Object); - var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var firstName = Faker.Name.Last(); + var lastName = Faker.Name.Last(); + var dateOfBirth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()); + var person = await TestData.CreatePerson(b => b.WithTrn().WithFirstName(firstName).WithLastName(lastName).WithDateOfBirth(dateOfBirth).WithNationalInsuranceNumber()); + + var user = await TestData.CreateOneLoginUser(personId: null, verifiedInfo: ([firstName, lastName], dateOfBirth)); + Clock.Advance(); var state = new SignInJourneyState(redirectUri: "/", serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); var journeyInstance = await CreateJourneyInstance(state); - var subject = TestData.CreateOneLoginUserSubject(); - var ticket = CreateOneLoginAuthenticationTicket(vtr: SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr, sub: subject, createCoreIdentityVc: true); - await helper.OnSignedInWithOneLogin(journeyInstance, ticket); + var authenticationTicket = CreateOneLoginAuthenticationTicket( + vtr: SignInJourneyHelper.AuthenticationOnlyVtr, + sub: user.Subject, + createCoreIdentityVc: false); + await helper.OnSignedInWithOneLogin(journeyInstance, authenticationTicket); await journeyInstance.UpdateStateAsync(state => state.SetNationalInsuranceNumber(true, person.NationalInsuranceNumber)); @@ -369,7 +427,7 @@ public Task TryMatchToTeachingRecord_MatchesSingleResult_ReturnsTrueAndUpdatesOn // Assert Assert.True(result); - var user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == subject); + user = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == user.Subject); Assert.Equal(Clock.UtcNow, user.FirstSignIn); Assert.Equal(Clock.UtcNow, user.LastSignIn); Assert.Equal(person.PersonId, user.PersonId); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs index 675c2aedd..8b8764648 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/TestBase.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using TeachingRecordSystem.Core.DataStore.Postgres; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; using TeachingRecordSystem.Core.Services.TrsDataSync; using TeachingRecordSystem.FormFlow; using TeachingRecordSystem.FormFlow.State; @@ -146,6 +147,15 @@ public AuthenticationTicket CreateOneLoginAuthenticationTicket( return new AuthenticationTicket(principal, properties, authenticationScheme: "OneLogin"); } + public AuthenticationTicket CreateOneLoginAuthenticationTicket(string vtr, OneLoginUser user) => + CreateOneLoginAuthenticationTicket( + vtr, + user.Subject, + user.Email, + user.VerifiedNames?.First().First(), + user.VerifiedNames?.First().Last(), + user.VerifiedDatesOfBirth?.First()); + public SignInJourneyState CreateNewState(string redirectUri = "/") => new(redirectUri, serviceName: "Test Service", serviceUrl: "https://service", oneLoginAuthenticationScheme: "dummy"); } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateOneLoginUser.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateOneLoginUser.cs index 1695916a5..84dfa97e5 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateOneLoginUser.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateOneLoginUser.cs @@ -6,11 +6,31 @@ namespace TeachingRecordSystem.TestCommon; public partial class TestData { + public Task CreateOneLoginUser(CreatePersonResult createPersonResult, string? subject = null, string? email = null) => + CreateOneLoginUser( + createPersonResult.PersonId, + subject, + email, + verifiedInfo: ([createPersonResult.FirstName, createPersonResult.LastName], createPersonResult.DateOfBirth)); + + public Task CreateOneLoginUser(string? subject = null, string? email = null, bool verified = false) => + CreateOneLoginUser( + personId: null, + subject, + email, + verifiedInfo: verified ? ([Faker.Name.First(), Faker.Name.Last()], DateOnly.FromDateTime(Faker.Identification.DateOfBirth())) : null); + public Task CreateOneLoginUser( Guid? personId, string? subject = null, - string? email = null) + string? email = null, + (string[] Name, DateOnly DateOfBirth)? verifiedInfo = null) { + if (personId is not null && verifiedInfo is null) + { + throw new ArgumentException("OneLoginUser with a Person must be verified.", nameof(verifiedInfo)); + } + return WithDbContext(async dbContext => { subject ??= CreateOneLoginUserSubject(); @@ -25,6 +45,14 @@ public Task CreateOneLoginUser( PersonId = personId }; + if (verifiedInfo is not null) + { + user.VerifiedOn = Clock.UtcNow; + user.VerificationRoute = OneLoginUserVerificationRoute.OneLogin; + user.VerifiedNames = [verifiedInfo!.Value.Name]; + user.VerifiedDatesOfBirth = [verifiedInfo!.Value.DateOfBirth]; + } + dbContext.OneLoginUsers.Add(user); await dbContext.SaveChangesAsync();