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("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") + .IsRequired() + .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.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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .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/20240318122634_OpenIddict.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240318122634_OpenIddict.cs new file mode 100644 index 000000000..bb91bf964 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240318122634_OpenIddict.cs @@ -0,0 +1,161 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + /// + public partial class OpenIddict : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "oidc_applications", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + application_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + client_id = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + client_secret = table.Column(type: "text", nullable: true), + client_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + concurrency_token = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + consent_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + display_name = table.Column(type: "text", nullable: true), + display_names = table.Column(type: "text", nullable: true), + json_web_key_set = table.Column(type: "text", nullable: true), + permissions = table.Column(type: "text", nullable: true), + post_logout_redirect_uris = table.Column(type: "text", nullable: true), + properties = table.Column(type: "text", nullable: true), + redirect_uris = table.Column(type: "text", nullable: true), + requirements = table.Column(type: "text", nullable: true), + settings = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_oidc_applications", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "oidc_scopes", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + concurrency_token = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + description = table.Column(type: "text", nullable: true), + descriptions = table.Column(type: "text", nullable: true), + display_name = table.Column(type: "text", nullable: true), + display_names = table.Column(type: "text", nullable: true), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + properties = table.Column(type: "text", nullable: true), + resources = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_oidc_scopes", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "oidc_authorizations", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + application_id = table.Column(type: "uuid", nullable: true), + concurrency_token = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + creation_date = table.Column(type: "timestamp with time zone", nullable: true), + properties = table.Column(type: "text", nullable: true), + scopes = table.Column(type: "text", nullable: true), + status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_oidc_authorizations", x => x.id); + table.ForeignKey( + name: "fk_oidc_authorizations_oidc_applications_application_id", + column: x => x.application_id, + principalTable: "oidc_applications", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "oidc_tokens", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + application_id = table.Column(type: "uuid", nullable: true), + authorization_id = table.Column(type: "uuid", nullable: true), + concurrency_token = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + creation_date = table.Column(type: "timestamp with time zone", nullable: true), + expiration_date = table.Column(type: "timestamp with time zone", nullable: true), + payload = table.Column(type: "text", nullable: true), + properties = table.Column(type: "text", nullable: true), + redemption_date = table.Column(type: "timestamp with time zone", nullable: true), + reference_id = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_oidc_tokens", x => x.id); + table.ForeignKey( + name: "fk_oidc_tokens_oidc_applications_application_id", + column: x => x.application_id, + principalTable: "oidc_applications", + principalColumn: "id"); + table.ForeignKey( + name: "fk_oidc_tokens_oidc_authorizations_authorization_id", + column: x => x.authorization_id, + principalTable: "oidc_authorizations", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_oidc_applications_client_id", + table: "oidc_applications", + column: "client_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_oidc_authorizations_application_id_status_subject_type", + table: "oidc_authorizations", + columns: new[] { "application_id", "status", "subject", "type" }); + + migrationBuilder.CreateIndex( + name: "ix_oidc_scopes_name", + table: "oidc_scopes", + column: "name", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_oidc_tokens_application_id_status_subject_type", + table: "oidc_tokens", + columns: new[] { "application_id", "status", "subject", "type" }); + + migrationBuilder.CreateIndex( + name: "ix_oidc_tokens_reference_id", + table: "oidc_tokens", + column: "reference_id", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "oidc_scopes"); + + migrationBuilder.DropTable( + name: "oidc_tokens"); + + migrationBuilder.DropTable( + name: "oidc_authorizations"); + + migrationBuilder.DropTable( + name: "oidc_applications"); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs index a79a455f7..524f3566d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs @@ -18,11 +18,273 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .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") @@ -1525,6 +1787,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -1659,6 +1948,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs index c1155e421..c14a40308 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs @@ -1,4 +1,7 @@ using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using OpenIddict.EntityFrameworkCore.Models; +using static OpenIddict.Abstractions.OpenIddictConstants; namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; @@ -78,6 +81,38 @@ public void EnsureConfiguredForOneLogin() throw new InvalidOperationException($"{nameof(OneLoginPostLogoutRedirectUriPath)} is not set."); } } + + public OpenIddictEntityFrameworkCoreApplication? ToOpenIddictApplication() + { + if (!IsOidcClient) + { + return null; + } + + return new OpenIddictEntityFrameworkCoreApplication() + { + ApplicationType = ApplicationTypes.Web, + ClientId = ClientId, + ClientSecret = ClientSecret, + ClientType = ClientTypes.Confidential, + ConsentType = ConsentTypes.Implicit, + DisplayName = Name, + Id = UserId, + Permissions = CreateJsonArray( + Permissions.Endpoints.Authorization, + Permissions.Endpoints.Token, + Permissions.Endpoints.Logout, + Permissions.GrantTypes.AuthorizationCode, + Permissions.ResponseTypes.Code, + Permissions.Scopes.Email, + Permissions.Scopes.Profile), + RedirectUris = CreateJsonArray(RedirectUris!.ToArray()), + PostLogoutRedirectUris = CreateJsonArray(PostLogoutRedirectUris!.ToArray()), + Requirements = CreateJsonArray(Requirements.Features.ProofKeyForCodeExchange) + }; + + static string CreateJsonArray(params string[] values) => JsonSerializer.Serialize(values); + } } public class SystemUser : UserBase diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs index c4b9365d1..8db93eddb 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/TrsDbContext.cs @@ -1,6 +1,8 @@ using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using OpenIddict.EntityFrameworkCore.Models; using TeachingRecordSystem.Core.DataStore.Postgres.Models; +using TeachingRecordSystem.Core.Infrastructure.EntityFramework; using Establishment = TeachingRecordSystem.Core.DataStore.Postgres.Models.Establishment; using User = TeachingRecordSystem.Core.DataStore.Postgres.Models.User; @@ -84,7 +86,9 @@ public static void ConfigureOptions(DbContextOptionsBuilder optionsBuilder, stri } optionsBuilder - .UseSnakeCaseNamingConvention(); + .UseSnakeCaseNamingConvention() + .UseOpenIddict() + .AddInterceptors(new PopulateOidcApplicationInterceptor()); } public void AddEvent(EventBase @event, DateTime? inserted = null) @@ -100,6 +104,23 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(TrsDbContext).Assembly); + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var clrType = entityType.ClrType; + + if (clrType.Assembly == typeof(OpenIddictEntityFrameworkCoreApplication).Assembly) + { + entityType.SetTableName(clrType.Name.Split("`")[0] switch + { + nameof(OpenIddictEntityFrameworkCoreApplication) => "oidc_applications", + nameof(OpenIddictEntityFrameworkCoreAuthorization) => "oidc_authorizations", + nameof(OpenIddictEntityFrameworkCoreScope) => "oidc_scopes", + nameof(OpenIddictEntityFrameworkCoreToken) => "oidc_tokens", + _ => throw new NotSupportedException($"Cannot configure table name for {clrType.Name}.") + }); + } + } } private static DbContextOptions CreateOptions(string connectionString, int? commandTimeout) diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Infrastructure/EntityFramework/PopulateOidcApplicationInterceptor.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Infrastructure/EntityFramework/PopulateOidcApplicationInterceptor.cs new file mode 100644 index 000000000..f26a2660b --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Infrastructure/EntityFramework/PopulateOidcApplicationInterceptor.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Diagnostics; +using OpenIddict.EntityFrameworkCore.Models; +using TeachingRecordSystem.Core.DataStore.Postgres.Models; + +namespace TeachingRecordSystem.Core.Infrastructure.EntityFramework; + +internal class PopulateOidcApplicationInterceptor : SaveChangesInterceptor +{ + public override async ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + var context = eventData.Context!; + var applicationUserEntries = context.ChangeTracker.Entries(); + + foreach (var entry in applicationUserEntries.ToArray()) + { + if (entry.CurrentValues.GetValue(nameof(ApplicationUser.IsOidcClient))) + { + var openIddictAppSet = context.Set>(); + + var existingOpenIddictApp = await openIddictAppSet.SingleOrDefaultAsync(u => u.Id == entry.Entity.UserId); + + if (existingOpenIddictApp is not null) + { + openIddictAppSet.Remove(existingOpenIddictApp); + } + + var newOpenIddictApp = entry.Entity.ToOpenIddictApplication()!; + newOpenIddictApp.ConcurrencyToken = existingOpenIddictApp?.ConcurrencyToken; + + var openIddictAppEntry = openIddictAppSet.Attach(newOpenIddictApp); + + openIddictAppEntry.State = existingOpenIddictApp is null ? EntityState.Added : EntityState.Modified; + } + } + + return result; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/TeachingRecordSystem.Core.csproj b/TeachingRecordSystem/src/TeachingRecordSystem.Core/TeachingRecordSystem.Core.csproj index 1d25d772c..7223fbd1c 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/TeachingRecordSystem.Core.csproj +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/TeachingRecordSystem.Core.csproj @@ -77,6 +77,7 @@ + diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs index 9ad957cc5..187878c35 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs @@ -204,6 +204,7 @@ key is nameof(ClientId) or nameof(ClientSecret) or nameof(RedirectUris) or nameo _user.Name = Name!; _user.ApiRoles = newApiRoles; + _user.IsOidcClient = IsOidcClient; if (IsOidcClient) { diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs index bc2e45479..b34680f56 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/SignInJourneyHelperTests.cs @@ -392,7 +392,6 @@ await journeyInstance.UpdateStateAsync(state => Assert.NotNull(state.AuthenticationTicket); Assert.Equal(person.Trn, state.AuthenticationTicket.Principal.FindFirstValue(ClaimTypes.Trn)); - Assert.Equal(person.PersonId.ToString(), state.AuthenticationTicket.Principal.FindFirstValue(ClaimTypes.PersonId)); }); private SignInJourneyHelper CreateHelper(TrsDbContext dbContext, IPersonSearchService? personSearchService = null) diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs index 2ff59372f..2a9e16b25 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs @@ -15,6 +15,10 @@ public async Task CreateApplicationUser( name ??= GenerateApplicationUserName(); apiRoles ??= []; hasOneLoginSettings ??= false; + string? clientId = null; + string? clientSecret = null; + List? redirectUris = null; + List? postLogoutRedirectUris = null; string? oneLoginClientId = null; string? oneLoginPrivateKeyPem = null; string? oneLoginAuthenticationSchemeName = null; @@ -23,6 +27,10 @@ public async Task CreateApplicationUser( if (hasOneLoginSettings == true) { + clientId = Guid.NewGuid().ToString(); + clientSecret = Guid.NewGuid().ToString(); + redirectUris = ["https://localhost:3000/callback"]; + postLogoutRedirectUris = ["https://localhost:3000/logout-callback"]; oneLoginClientId = Guid.NewGuid().ToString(); oneLoginPrivateKeyPem = GeneratePrivateKeyPem(); oneLoginAuthenticationSchemeName = Guid.NewGuid().ToString(); @@ -36,6 +44,10 @@ public async Task CreateApplicationUser( UserId = Guid.NewGuid(), ApiRoles = apiRoles, IsOidcClient = hasOneLoginSettings.Value, + ClientId = clientId, + ClientSecret = clientSecret, + RedirectUris = redirectUris, + PostLogoutRedirectUris = postLogoutRedirectUris, OneLoginClientId = oneLoginClientId, OneLoginPrivateKeyPem = oneLoginPrivateKeyPem, OneLoginAuthenticationSchemeName = oneLoginAuthenticationSchemeName, @@ -50,7 +62,7 @@ public async Task CreateApplicationUser( EventId = Guid.NewGuid(), RaisedBy = SystemUser.SystemUserId, CreatedUtc = Clock.UtcNow, - ApplicationUser = Core.Events.Models.ApplicationUser.FromModel(user) + ApplicationUser = EventModels.ApplicationUser.FromModel(user) }; dbContext.AddEvent(@event);