diff --git a/TeachingRecordSystem/Directory.Packages.props b/TeachingRecordSystem/Directory.Packages.props
index 8f5036060..7aa664a14 100644
--- a/TeachingRecordSystem/Directory.Packages.props
+++ b/TeachingRecordSystem/Directory.Packages.props
@@ -36,8 +36,8 @@
-
-
+
+
@@ -62,6 +62,8 @@
+
+
@@ -90,4 +92,4 @@
-
\ No newline at end of file
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/ClaimTypes.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/ClaimTypes.cs
index 4e839a0ec..5b6fda69f 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/ClaimTypes.cs
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/ClaimTypes.cs
@@ -2,6 +2,7 @@ namespace TeachingRecordSystem.AuthorizeAccess;
public static class ClaimTypes
{
+ public const string Email = "email";
+ public const string Subject = "sub";
public const string Trn = "trn";
- public const string PersonId = "person_id";
}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Controllers/OidcController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Controllers/OidcController.cs
new file mode 100644
index 000000000..02ddefa2c
--- /dev/null
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Controllers/OidcController.cs
@@ -0,0 +1,150 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Abstractions;
+using OpenIddict.Server.AspNetCore;
+using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Security;
+using TeachingRecordSystem.Core.DataStore.Postgres;
+using static OpenIddict.Abstractions.OpenIddictConstants;
+
+namespace TeachingRecordSystem.AuthorizeAccess.Controllers;
+
+public class OidcController(
+ TrsDbContext dbContext,
+ IOpenIddictAuthorizationManager authorizationManager,
+ IOpenIddictScopeManager scopeManager) : Controller
+{
+ [HttpGet("~/connect/authorize")]
+ [HttpPost("~/connect/authorize")]
+ [IgnoreAntiforgeryToken]
+ public async Task Authorize()
+ {
+ var request = HttpContext.GetOpenIddictServerRequest() ??
+ throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
+
+ var clientId = request.ClientId!;
+ var client = await dbContext.ApplicationUsers.SingleAsync(u => u.ClientId == clientId);
+
+ var childAuthenticationScheme = AuthenticationSchemes.MatchToTeachingRecord;
+ var authenticateResult = await HttpContext.AuthenticateAsync(childAuthenticationScheme);
+
+ if (!authenticateResult.Succeeded)
+ {
+ var parameters = Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList();
+
+
+ var authenticationProperties = new AuthenticationProperties()
+ {
+ RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
+ };
+ authenticationProperties.Items.Add("OneLoginAuthenticationScheme", client.OneLoginAuthenticationSchemeName);
+
+ return Challenge(authenticationProperties, childAuthenticationScheme);
+ }
+
+ var user = authenticateResult.Principal;
+ var subject = user.FindFirstValue(ClaimTypes.Subject) ??
+ throw new InvalidOperationException($"Principal does not contain a '{ClaimTypes.Subject}' claim.");
+
+ var authorizations = await authorizationManager.FindAsync(
+ subject: subject,
+ client: client.UserId.ToString(),
+ status: Statuses.Valid,
+ type: AuthorizationTypes.Permanent,
+ scopes: request.GetScopes()).ToListAsync();
+
+ var identity = new ClaimsIdentity(
+ claims: user.Claims,
+ authenticationType: TokenValidationParameters.DefaultAuthenticationType,
+ nameType: ClaimTypes.Subject,
+ roleType: null);
+
+ identity.SetScopes(request.GetScopes());
+ identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
+
+ var authorization = authorizations.LastOrDefault();
+ authorization ??= await authorizationManager.CreateAsync(
+ identity: identity,
+ subject: subject,
+ client: client.UserId.ToString(),
+ type: AuthorizationTypes.Permanent,
+ scopes: identity.GetScopes());
+
+ identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
+ identity.SetDestinations(GetDestinations);
+
+ return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+
+ [HttpPost("~/connect/token")]
+ [IgnoreAntiforgeryToken]
+ [Produces("application/json")]
+ public async Task Token()
+ {
+ var request = HttpContext.GetOpenIddictServerRequest() ??
+ throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
+
+ if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
+ {
+ var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+
+ var identity = new ClaimsIdentity(
+ result.Principal!.Claims,
+ authenticationType: TokenValidationParameters.DefaultAuthenticationType,
+ nameType: ClaimTypes.Subject,
+ roleType: null);
+
+ identity.SetDestinations(GetDestinations);
+
+ return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+
+ throw new InvalidOperationException("The specified grant type is not supported.");
+ }
+
+ [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
+ [HttpGet("~/connect/userinfo")]
+ [HttpPost("~/connect/userinfo")]
+ [Produces("application/json")]
+ public IActionResult UserInfo()
+ {
+ var claims = new Dictionary(StringComparer.Ordinal)
+ {
+ [ClaimTypes.Subject] = User.GetClaim(ClaimTypes.Subject)!,
+ [ClaimTypes.Trn] = User.GetClaim(ClaimTypes.Trn)!
+ };
+
+ if (User.HasScope(Scopes.Email))
+ {
+ claims[ClaimTypes.Email] = User.GetClaim(ClaimTypes.Email)!;
+ }
+
+ return Ok(claims);
+ }
+
+ private static IEnumerable GetDestinations(Claim claim)
+ {
+ switch (claim.Type)
+ {
+ case ClaimTypes.Subject:
+ case ClaimTypes.Trn:
+ yield return Destinations.AccessToken;
+ yield return Destinations.IdentityToken;
+ yield break;
+
+ case ClaimTypes.Email:
+ if (claim.Subject!.HasScope(Scopes.Profile))
+ {
+ yield return Destinations.IdentityToken;
+ }
+
+ yield break;
+
+ default:
+ yield break;
+ }
+ }
+}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Oidc/ApplicationManager.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Oidc/ApplicationManager.cs
new file mode 100644
index 000000000..308283de6
--- /dev/null
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Oidc/ApplicationManager.cs
@@ -0,0 +1,23 @@
+using Microsoft.Extensions.Options;
+using OpenIddict.Abstractions;
+using OpenIddict.Core;
+using OpenIddict.EntityFrameworkCore.Models;
+
+namespace TeachingRecordSystem.AuthorizeAccess.Infrastructure.Oidc;
+
+public class ApplicationManager : OpenIddictApplicationManager>
+{
+ public ApplicationManager(
+ IOpenIddictApplicationCache> cache,
+ ILogger>> logger,
+ IOptionsMonitor options,
+ IOpenIddictApplicationStoreResolver resolver) : base(cache, logger, options, resolver)
+ {
+ }
+
+ protected override ValueTask ObfuscateClientSecretAsync(string secret, CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ protected override ValueTask ValidateClientSecretAsync(string secret, string comparand, CancellationToken cancellationToken = default) =>
+ ValueTask.FromResult(secret.Equals(comparand));
+}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs
index 9e8c554e2..21a42923a 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs
@@ -12,13 +12,16 @@
using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Filters;
using TeachingRecordSystem.AuthorizeAccess.Infrastructure.FormFlow;
using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Logging;
+using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Oidc;
using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Security;
using TeachingRecordSystem.AuthorizeAccess.TagHelpers;
+using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.Dqt;
using TeachingRecordSystem.Core.Services.PersonSearch;
using TeachingRecordSystem.FormFlow;
using TeachingRecordSystem.ServiceDefaults;
using TeachingRecordSystem.SupportUi.Infrastructure.FormFlow;
+using static OpenIddict.Abstractions.OpenIddictConstants;
var builder = WebApplication.CreateBuilder(args);
@@ -58,6 +61,49 @@
.AddSingleton(sp => sp.GetRequiredService());
}
+builder.Services.AddOpenIddict()
+ .AddCore(options =>
+ {
+ options
+ .UseEntityFrameworkCore()
+ .UseDbContext()
+ .ReplaceDefaultEntities();
+
+ options.ReplaceApplicationManager();
+ })
+ .AddServer(options =>
+ {
+ //options.SetIssuer(); // TODO
+
+ options
+ .SetAuthorizationEndpointUris("connect/authorize")
+ .SetLogoutEndpointUris("connect/logout")
+ .SetTokenEndpointUris("connect/token")
+ .SetUserinfoEndpointUris("connect/userinfo");
+
+ // TODO - add teaching record scopes
+ options.RegisterScopes(Scopes.Email, Scopes.Profile);
+
+ options.AllowAuthorizationCodeFlow();
+
+ options.DisableAccessTokenEncryption();
+ options.SetAccessTokenLifetime(TimeSpan.FromHours(1));
+
+ //if (!builder.Environment.IsProduction())
+ {
+ options
+ .AddDevelopmentEncryptionCertificate()
+ .AddDevelopmentSigningCertificate();
+ }
+
+ options.UseAspNetCore()
+ .EnableAuthorizationEndpointPassthrough()
+ .EnableLogoutEndpointPassthrough()
+ .EnableTokenEndpointPassthrough()
+ .EnableUserinfoEndpointPassthrough()
+ .EnableStatusCodePagesIntegration();
+ });
+
builder.Services
.AddRazorPages()
.AddMvcOptions(options =>
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs
index 69c701b70..df3c6cde4 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs
@@ -89,8 +89,7 @@ public async Task CreateOrUpdateOneLoginUser(JourneyInstance
- CreateAndAssignPrincipal(state, oneLoginUser.Person.PersonId, oneLoginUser.Person.Trn!));
+ await journeyInstance.UpdateStateAsync(state => CreateAndAssignPrincipal(state, oneLoginUser.Person.Trn!));
}
}
else
@@ -170,8 +169,7 @@ public async Task TryMatchToTeachingRecord(JourneyInstance
- CreateAndAssignPrincipal(state, matchedPersonId, matchedTrn));
+ await journeyInstance.UpdateStateAsync(state => CreateAndAssignPrincipal(state, matchedTrn));
return true;
}
@@ -185,22 +183,26 @@ await journeyInstance.UpdateStateAsync(state =>
string.IsNullOrEmpty(value) ? null : new(value.Where(char.IsAsciiDigit).ToArray());
}
- private static void CreateAndAssignPrincipal(SignInJourneyState state, Guid personId, string trn)
+ private static void CreateAndAssignPrincipal(SignInJourneyState state, string trn)
{
if (state.OneLoginAuthenticationTicket is null)
{
throw new InvalidOperationException("User is not authenticated with One Login.");
}
- var oneLoginIdentity = (ClaimsIdentity)state.OneLoginAuthenticationTicket.Principal.Identity!;
+ var oneLoginPrincipal = state.OneLoginAuthenticationTicket.Principal;
- var teachingRecordIdentity = new ClaimsIdentity(new[]
- {
- new Claim(ClaimTypes.Trn, trn),
- new Claim(ClaimTypes.PersonId, personId.ToString())
- });
+ var teachingRecordIdentity = new ClaimsIdentity(
+ [
+ new Claim(ClaimTypes.Subject, oneLoginPrincipal.FindFirstValue("sub")!),
+ new Claim(ClaimTypes.Trn, trn),
+ new Claim(ClaimTypes.Email, oneLoginPrincipal.FindFirstValue("email")!)
+ ],
+ authenticationType: "Authorize access to a teaching record",
+ nameType: "sub",
+ roleType: null);
- var principal = new ClaimsPrincipal([oneLoginIdentity, teachingRecordIdentity]);
+ var principal = new ClaimsPrincipal(teachingRecordIdentity);
state.AuthenticationTicket = new AuthenticationTicket(principal, state.AuthenticationProperties, AuthenticationSchemes.MatchToTeachingRecord);
}
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj
index 65ae29be1..a7d0d4324 100644
--- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj
@@ -9,6 +9,7 @@
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240318122634_OpenIddict.Designer.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240318122634_OpenIddict.Designer.cs
new file mode 100644
index 000000000..ea1f025bf
--- /dev/null
+++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240318122634_OpenIddict.Designer.cs
@@ -0,0 +1,1993 @@
+//
+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("20240318122634_OpenIddict")]
+ partial class OpenIddict
+ {
+ ///
+ 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("EstablishmentStatusCode")
+ .HasColumnType("integer")
+ .HasColumnName("establishment_status_code");
+
+ b.Property("EstablishmentStatusName")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("establishment_status_name");
+
+ b.Property("EstablishmentTypeCode")
+ .IsRequired()
+ .HasMaxLength(3)
+ .HasColumnType("character varying(3)")
+ .HasColumnName("establishment_type_code");
+
+ b.Property("EstablishmentTypeGroupCode")
+ .HasColumnType("integer")
+ .HasColumnName("establishment_type_group_code");
+
+ b.Property("EstablishmentTypeGroupName")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("establishment_type_group_name");
+
+ b.Property("EstablishmentTypeName")
+ .IsRequired()
+ .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")
+ .IsRequired()
+ .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("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.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.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.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