Skip to content

Commit

Permalink
Merge branch 'main' into data-integration-tech-arch
Browse files Browse the repository at this point in the history
  • Loading branch information
aje54 authored Dec 18, 2023
2 parents 20bd33e + c98a26d commit 222f846
Show file tree
Hide file tree
Showing 136 changed files with 6,680 additions and 1,174 deletions.
4 changes: 3 additions & 1 deletion TeachingRecordSystem/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
<PackageVersion Include="GovUk.Frontend.AspNetCore" Version="1.4.0" />
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.6" />
<PackageVersion Include="Hangfire.Core" Version="1.8.6" />
<PackageVersion Include="Hangfire.NetCore" Version="1.8.6" />
<PackageVersion Include="Hangfire.PostgreSql" Version="1.20.4" />
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
<PackageVersion Include="IdentityModel" Version="6.2.0" />
<PackageVersion Include="idunno.Authentication.Basic" Version="2.3.1" />
<PackageVersion Include="Joonasw.AspNetCore.SecurityHeaders" Version="5.0.0" />
Expand Down Expand Up @@ -84,4 +86,4 @@
<PackageVersion Include="Xunit.DependencyInjection" Version="8.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.4" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddRedis(
this IServiceCollection services,
IWebHostEnvironment environment,
IConfiguration configuration,
IHealthChecksBuilder healthChecksBuilder)
IConfiguration configuration)
{
if (environment.IsProduction())
{
Expand All @@ -16,7 +15,7 @@ public static IServiceCollection AddRedis(
services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(connectionString));
services.AddStackExchangeRedisCache(options => options.Configuration = connectionString);

healthChecksBuilder.AddRedis(connectionString);
services.AddHealthChecks().AddRedis(connectionString);
}

return services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ public static class AuthorizationPolicies
{
public const string ApiKey = nameof(ApiKey);
public const string IdentityUserWithTrn = nameof(IdentityUserWithTrn);
public const string Hangfire = nameof(Hangfire);
public const string GetPerson = nameof(GetPerson);
public const string UpdatePerson = nameof(UpdatePerson);
public const string UpdateNpq = nameof(UpdateNpq);
Expand All @@ -15,16 +14,13 @@ public static bool IsApiKeyAuthentication(string policy)
switch (policy)
{
case ApiKey:
case Hangfire:
case GetPerson:
case UpdatePerson:
case UpdateNpq:
case UnlockPerson:
return true;
case IdentityUserWithTrn:
default:
return false;

}
}
}
71 changes: 12 additions & 59 deletions TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
using System.Security.Claims;
using FluentValidation;
using FluentValidation.AspNetCore;
using Hangfire;
using Hangfire.PostgreSql;
using idunno.Authentication.Basic;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.PowerPlatform.Dataverse.Client;
using Npgsql;
using TeachingRecordSystem.Api.Endpoints.IdentityWebHooks;
using TeachingRecordSystem.Api.Infrastructure.ApplicationModel;
using TeachingRecordSystem.Api.Infrastructure.Filters;
Expand All @@ -20,11 +17,8 @@
using TeachingRecordSystem.Api.Infrastructure.Redis;
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.Validation;
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.Dqt;
using TeachingRecordSystem.Core.Infrastructure;
using TeachingRecordSystem.Core.Jobs;
using TeachingRecordSystem.Core.Services.AccessYourQualifications;
using TeachingRecordSystem.Core.Services.Certificates;
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;
using TeachingRecordSystem.Core.Services.Notify;
Expand All @@ -49,12 +43,6 @@ public static void Main(string[] args)

builder.ConfigureLogging();

string pgConnectionString = new NpgsqlConnectionStringBuilder(configuration.GetRequiredValue("ConnectionStrings:DefaultConnection"))
{
// We rely on error details to get the offending duplicate key values in the TrsDataSyncHelper
IncludeErrorDetail = true
}.ConnectionString;

services.AddAuthentication(ApiKeyAuthenticationHandler.AuthenticationScheme)
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationHandler.AuthenticationScheme, _ => { })
.AddJwtBearer(options =>
Expand Down Expand Up @@ -108,22 +96,15 @@ public static void Main(string[] args)
.RequireClaim(ClaimTypes.Name));

options.AddPolicy(
AuthorizationPolicies.IdentityUserWithTrn,
policy => policy
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAssertion(ctx =>
{
var scopes = (ctx.User.FindFirstValue("scope") ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries);
return scopes.Contains("dqt:read");
})
.RequireClaim("trn"));

options.AddPolicy(
AuthorizationPolicies.Hangfire,
AuthorizationPolicies.IdentityUserWithTrn,
policy => policy
.AddAuthenticationSchemes(BasicAuthenticationDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
);
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAssertion(ctx =>
{
var scopes = (ctx.User.FindFirstValue("scope") ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries);
return scopes.Contains("dqt:read");
})
.RequireClaim("trn"));

options.AddPolicy(
AuthorizationPolicies.GetPerson,
Expand Down Expand Up @@ -187,9 +168,6 @@ public static void Main(string[] args)

services.AddOpenApi(configuration);

var healthCheckBuilder = services.AddHealthChecks()
.AddNpgSql(pgConnectionString);

services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());
services.AddSingleton<IApiClientRepository, ConfigurationApiClientRepository>();
services.AddSingleton<ICurrentClientProvider, ClaimsPrincipalCurrentClientProvider>();
Expand All @@ -202,35 +180,14 @@ public static void Main(string[] args)
client.Timeout = TimeSpan.FromSeconds(30);
});

services.AddDbContext<TrsDbContext>(
options => TrsDbContext.ConfigureOptions(options, pgConnectionString),
contextLifetime: ServiceLifetime.Transient,
optionsLifetime: ServiceLifetime.Singleton);

services.AddDbContextFactory<TrsDbContext>(options => TrsDbContext.ConfigureOptions(options, pgConnectionString));

services.AddDatabaseDeveloperPageExceptionFilter();

builder.AddBlobStorage();

builder.AddDistributedLocks();

if (!env.IsUnitTests() && !env.IsEndToEndTests())
{
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UsePostgreSqlStorage(o => o.UseNpgsqlConnection(pgConnectionString)));

services.AddHangfireServer();
}

services.AddTrnGenerationApi(configuration);
services.AddIdentityApi(configuration, env);
services.AddAccessYourQualifications(configuration, env);
services.AddAccessYourTeachingQualificationsOptions(configuration, env);
services.AddCertificateGeneration();
services.AddBackgroundJobs(env, configuration);
services.AddEmail(env, configuration);
services.AddCrmQueries();

Expand All @@ -241,10 +198,11 @@ public static void Main(string[] args)
services.AddDefaultServiceClient(ServiceLifetime.Transient, _ => crmServiceClient.Clone());
services.AddTransient<IDataverseAdapter, DataverseAdapter>();

healthCheckBuilder.AddCheck("CRM", () => crmServiceClient.IsReady ? HealthCheckResult.Healthy() : HealthCheckResult.Degraded());
services.AddHealthChecks()
.AddCheck("CRM", () => crmServiceClient.IsReady ? HealthCheckResult.Healthy() : HealthCheckResult.Degraded());
}

services.AddRedis(env, configuration, healthCheckBuilder);
services.AddRedis(env, configuration);
services.AddRateLimiting(env, configuration);

var app = builder.Build();
Expand Down Expand Up @@ -276,11 +234,6 @@ public static void Main(string[] args)

app.MapControllers();

if (!builder.Environment.IsUnitTests() && !builder.Environment.IsEndToEndTests())
{
app.MapHangfireDashboardWithAuthorizationPolicy(AuthorizationPolicies.Hangfire, "/_hangfire");
}

if (env.IsDevelopment())
{
app.UseMigrationsEndPoint();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" />
<PackageReference Include="FluentValidation.AspNetCore" />
<PackageReference Include="Hangfire.AspNetCore" />
<PackageReference Include="Hangfire.PostgreSql" />
<PackageReference Include="idunno.Authentication.Basic" />
<PackageReference Include="MediatR" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" />
<PackageReference Include="Moq" />
<PackageReference Include="NSwag.AspNetCore" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
using TeachingRecordSystem.Core.Dqt;
using TeachingRecordSystem.Core.Dqt.Models;
using TeachingRecordSystem.Core.Services.AccessYourQualifications;
using TeachingRecordSystem.Core.Services.GetAnIdentity.Api.Models;
using TeachingRecordSystem.Core.Services.GetAnIdentityApi;

Expand All @@ -30,22 +29,22 @@ public class GetOrCreateTrnRequestHandler : IRequestHandler<GetOrCreateTrnReques
private readonly ICurrentClientProvider _currentClientProvider;
private readonly IDistributedLockProvider _distributedLockProvider;
private readonly IGetAnIdentityApiClient _identityApiClient;
private readonly AccessYourQualificationsOptions _accessYourQualificationsOptions;
private readonly AccessYourTeachingQualificationsOptions _accessYourTeachingQualificationsOptions;

public GetOrCreateTrnRequestHandler(
TrsDbContext TrsDbContext,
IDataverseAdapter dataverseAdapter,
ICurrentClientProvider currentClientProvider,
IDistributedLockProvider distributedLockProvider,
IGetAnIdentityApiClient identityApiClient,
IOptions<AccessYourQualificationsOptions> accessYourQualificationsOptions)
IOptions<AccessYourTeachingQualificationsOptions> accessYourTeachingQualificationsOptions)
{
_trsDbContext = TrsDbContext;
_dataverseAdapter = dataverseAdapter;
_currentClientProvider = currentClientProvider;
_distributedLockProvider = distributedLockProvider;
_identityApiClient = identityApiClient;
_accessYourQualificationsOptions = accessYourQualificationsOptions.Value;
_accessYourTeachingQualificationsOptions = accessYourTeachingQualificationsOptions.Value;
}

public async Task<TrnRequestInfo> Handle(GetOrCreateTrnRequest request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -187,7 +186,7 @@ public async Task<TrnRequestInfo> Handle(GetOrCreateTrnRequest request, Cancella
QtsDate = qtsDate,
PotentialDuplicate = status == TrnRequestStatus.Pending,
SlugId = request.SlugId,
AccessYourTeachingQualificationsLink = trnToken is not null ? Option.Some($"{_accessYourQualificationsOptions.BaseAddress}{_accessYourQualificationsOptions.StartUrlPath}?trn_token={trnToken}") : default
AccessYourTeachingQualificationsLink = trnToken is not null ? Option.Some($"{_accessYourTeachingQualificationsOptions.BaseAddress}{_accessYourTeachingQualificationsOptions.StartUrlPath}?trn_token={trnToken}") : default
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.Dqt;
using TeachingRecordSystem.Core.Dqt.Models;
using TeachingRecordSystem.Core.Services.AccessYourQualifications;

namespace TeachingRecordSystem.Api.V2.Handlers;

Expand All @@ -18,18 +17,18 @@ public class GetTrnRequestHandler : IRequestHandler<GetTrnRequest, TrnRequestInf
private readonly TrsDbContext _trsDbContext;
private readonly IDataverseAdapter _dataverseAdapter;
private readonly ICurrentClientProvider _currentClientProvider;
private readonly AccessYourQualificationsOptions _accessYourQualificationsOptions;
private readonly AccessYourTeachingQualificationsOptions _accessYourTeachingQualificationsOptions;

public GetTrnRequestHandler(
TrsDbContext TrsDbContext,
IDataverseAdapter dataverseAdapter,
ICurrentClientProvider currentClientProvider,
IOptions<AccessYourQualificationsOptions> accessYourQualificationsOptions)
IOptions<AccessYourTeachingQualificationsOptions> accessYourTeachingQualificationsOptions)
{
_trsDbContext = TrsDbContext;
_dataverseAdapter = dataverseAdapter;
_currentClientProvider = currentClientProvider;
_accessYourQualificationsOptions = accessYourQualificationsOptions.Value;
_accessYourTeachingQualificationsOptions = accessYourTeachingQualificationsOptions.Value;
}

public async Task<TrnRequestInfo> Handle(GetTrnRequest request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -69,7 +68,7 @@ public async Task<TrnRequestInfo> Handle(GetTrnRequest request, CancellationToke
QtsDate = qtsDate,
PotentialDuplicate = status == TrnRequestStatus.Pending,
SlugId = teacher.dfeta_SlugId,
AccessYourTeachingQualificationsLink = trnRequest.TrnToken is not null ? Option.Some($"{_accessYourQualificationsOptions.BaseAddress}{_accessYourQualificationsOptions.StartUrlPath}?trn_token={trnRequest.TrnToken}") : default
AccessYourTeachingQualificationsLink = trnRequest.TrnToken is not null ? Option.Some($"{_accessYourTeachingQualificationsOptions.BaseAddress}{_accessYourTeachingQualificationsOptions.StartUrlPath}?trn_token={trnRequest.TrnToken}") : default
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using TeachingRecordSystem.Api.Infrastructure.Security;

namespace TeachingRecordSystem.Api.V3.Controllers;

[Route("trn-requests")]
[Authorize(Policy = AuthorizationPolicies.ApiKey)]
public class TrnRequestsController : ControllerBase
{
[HttpPost("")]
[OpenApiOperation(
operationId: "CreateTrnRequest",
summary: "Creates a TRN request",
description: """
Creates a new TRN request using the personally identifiable information in the request body.
If the request can be fulfilled immediately the response's status property will be 'Completed' and a TRN will also be returned.
Otherwise, the response's status property will be 'Pending' and the GET endpoint should be polled until a 'Completed' status is returned.
""")]
[ProducesResponseType(typeof(TrnRequestInfo), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public IActionResult CreateTrnRequest([FromBody] CreateTrnRequestBody request) => throw new NotImplementedException();

[HttpGet("")]
[OpenApiOperation(
operationId: "GetTrnRequest",
summary: "Get the TRN request's details",
description: """
Gets the TRN request for the requestId specified in the query string.
If the request's status is 'Completed' a TRN will also be returned.
""")]
[ProducesResponseType(typeof(TrnRequestInfo), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetTrnRequest([FromQuery] string requestId) => throw new NotImplementedException();

public record CreateTrnRequestBody
{
public required string RequestId { get; init; }
public required TrnRequestPerson Person { get; init; }
}

public record TrnRequestPerson
{
public required string FirstName { get; init; }
public string? MiddleName { get; init; }
public required string LastName { get; init; }
public required DateOnly DateOfBirth { get; init; }
public string? Email { get; init; }
public string? NationalInsuranceNumber { get; init; }
}

public record TrnRequestInfo
{
public required string RequestId { get; init; }
public required TrnRequestPerson Person { get; init; }
public required TrnRequestStatus Status { get; init; }
public string? Trn { get; init; }
}

public enum TrnRequestStatus
{
Pending = 0,
Completed = 1
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace TeachingRecordSystem.Core.Services.AccessYourQualifications;
namespace TeachingRecordSystem.Core;

public class AccessYourQualificationsOptions
public class AccessYourTeachingQualificationsOptions
{
[Required]
public required string BaseAddress { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ public class EventMapping : IEntityTypeConfiguration<Event>
public void Configure(EntityTypeBuilder<Event> builder)
{
builder.ToTable("events");
builder.Property(e => e.EventId).IsRequired().ValueGeneratedOnAdd();
builder.Property(e => e.EventName).IsRequired().HasMaxLength(200);
builder.Property(e => e.Created).IsRequired();
builder.Property(e => e.Payload).IsRequired().HasColumnType("jsonb");
Expand Down
Loading

0 comments on commit 222f846

Please sign in to comment.