Skip to content

Commit

Permalink
Link One Login user when TRN request is Completed (#1661)
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad authored Nov 7, 2024
1 parent 55be8af commit 5617151
Show file tree
Hide file tree
Showing 28 changed files with 10,499 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
using TeachingRecordSystem.Core.Dqt;
Expand All @@ -6,7 +7,7 @@

namespace TeachingRecordSystem.Api;

public class TrnRequestHelper(TrsDbContext dbContext, ICrmQueryDispatcher crmQueryDispatcher)
public class TrnRequestHelper(TrsDbContext dbContext, ICrmQueryDispatcher crmQueryDispatcher, IClock clock)
{
public async Task<GetTrnRequestResult?> GetTrnRequestInfo(Guid currentApplicationUserId, string requestId)
{
Expand All @@ -16,21 +17,66 @@ public class TrnRequestHelper(TrsDbContext dbContext, ICrmQueryDispatcher crmQue
var getContactByTrnRequestIdTask = crmQueryDispatcher.ExecuteQuery(
new GetContactByTrnRequestIdQuery(crmTrnRequestId, new Microsoft.Xrm.Sdk.Query.ColumnSet(Contact.Fields.ContactId, Contact.Fields.dfeta_TrnToken)));

// We can't have this running in parallel with getDbTrnRequestTask since they share a connection so make it continuation
var metadata = await getDbTrnRequestTask
.ContinueWith(_ => dbContext.TrnRequestMetadata
.SingleOrDefaultAsync(m => m.ApplicationUserId == currentApplicationUserId && m.RequestId == requestId))
.Unwrap();

if (await getDbTrnRequestTask is TrnRequest dbTrnRequest)
{
return new(dbTrnRequest.TeacherId, dbTrnRequest.TrnToken);
return new(dbTrnRequest.TeacherId, dbTrnRequest.TrnToken, metadata, currentApplicationUserId);
}

if (await getContactByTrnRequestIdTask is Contact contact)
{
return new(contact.ContactId!.Value, contact.dfeta_TrnToken);
return new(contact.ContactId!.Value, contact.dfeta_TrnToken, metadata, currentApplicationUserId);
}

return null;
}

public static string GetCrmTrnRequestId(Guid currentApplicationUserId, string requestId) =>
$"{currentApplicationUserId}::{requestId}";

public async Task EnsureOneLoginUserIsConnected(GetTrnRequestResult trnRequest, Contact contact)
{
if (trnRequest.Metadata?.VerifiedOneLoginUserSubject is not string oneLoginUserSubject)
{
return;
}

if (await dbContext.OneLoginUsers.AnyAsync(u => u.Subject == oneLoginUserSubject))
{
return;
}

Debug.Assert(contact.dfeta_TRN is not null);

var oneLoginUser = new OneLoginUser() { Subject = oneLoginUserSubject };

var verifiedName = new string[]
{
contact.HasStatedNames() ? contact.dfeta_StatedFirstName : contact.FirstName,
contact.HasStatedNames() ? contact.dfeta_StatedMiddleName : contact.MiddleName,
contact.HasStatedNames() ? contact.dfeta_StatedLastName : contact.LastName
};

var verifiedDateOfBirth = contact.BirthDate!.Value.ToDateOnlyWithDqtBstFix(isLocalTime: false);

oneLoginUser.SetVerified(
verifiedOn: clock.UtcNow,
OneLoginUserVerificationRoute.External,
verifiedByApplicationUserId: trnRequest.ApplicationUserId,
verifiedNames: [[.. verifiedName]],
verifiedDatesOfBirth: [verifiedDateOfBirth]);

oneLoginUser.SetMatched(contact.Id, OneLoginUserMatchRoute.TrnAllocation, matchedAttributes: null);

dbContext.OneLoginUsers.Add(oneLoginUser);

await dbContext.SaveChangesAsync();
}
}

public record GetTrnRequestResult(Guid ContactId, string? TrnToken);
public record GetTrnRequestResult(Guid ContactId, string? TrnToken, TrnRequestMetadata? Metadata, Guid ApplicationUserId);
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class GetTrnRequestHandler(
Contact.Fields.FirstName,
Contact.Fields.MiddleName,
Contact.Fields.LastName,
Contact.Fields.dfeta_StatedFirstName,
Contact.Fields.dfeta_StatedMiddleName,
Contact.Fields.dfeta_StatedLastName,
Contact.Fields.EMailAddress1,
Contact.Fields.dfeta_NINumber,
Contact.Fields.BirthDate,
Expand All @@ -40,6 +43,13 @@ public class GetTrnRequestHandler(

var status = !string.IsNullOrEmpty(contact.dfeta_TRN) ? TrnRequestStatus.Completed : TrnRequestStatus.Pending;

// If we have metadata for the One Login user, ensure they're added to the OneLoginUsers table.
// FUTURE: when TRN requests are handled exclusively in TRS this should be done at the point the task is resolved instead of here.
if (status == TrnRequestStatus.Completed)
{
await trnRequestHelper.EnsureOneLoginUserIsConnected(trnRequest, contact);
}

return new TrnRequestInfo()
{
RequestId = command.RequestId.ToString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public async Task<IActionResult> UserInfo()

if (User.HasScope(Scopes.Email))
{
claims.Add(ClaimTypes.Email, oneLoginUser.Email);
claims.Add(ClaimTypes.Email, oneLoginUser.Email!);
}

if (oneLoginUser.VerificationRoute == OneLoginUserVerificationRoute.OneLogin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public async Task<IActionResult> OnPost()

if (_oneLoginUser!.PersonId is not null && DetachPerson)
{
_oneLoginUser.PersonId = null;
_oneLoginUser.ClearMatchedPerson();
}

if (IdentityVerified)
Expand All @@ -125,10 +125,7 @@ public async Task<IActionResult> OnPost()
}
else
{
_oneLoginUser!.VerifiedOn = null;
_oneLoginUser.VerificationRoute = null;
_oneLoginUser.VerifiedNames = null;
_oneLoginUser.VerifiedDatesOfBirth = null;
_oneLoginUser.ClearVerifiedInfo();

await JourneyInstance!.UpdateStateAsync(state => state.ClearVerified());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,16 @@ public async Task OnUserAuthenticated(JourneyInstance<SignInJourneyState> journe
}
else
{
oneLoginUser.FirstOneLoginSignIn ??= clock.UtcNow;
oneLoginUser.LastOneLoginSignIn = clock.UtcNow;

// Email may have changed since the last sign in - ensure we update it.
// Email may have changed since the last sign in or we may have never had it (e.g. if we got the user ID over our API).
// TODO Should we emit an event if it has changed?
oneLoginUser.Email = email;

if (oneLoginUser.PersonId is not null)
{
oneLoginUser.FirstSignIn ??= clock.UtcNow;
oneLoginUser.LastSignIn = clock.UtcNow;
}
}
Expand Down Expand Up @@ -149,10 +151,7 @@ public async Task OnUserVerifiedCore(
var sub = journeyInstance.State.OneLoginAuthenticationTicket!.Principal.FindFirstValue("sub") ?? throw new InvalidOperationException("No sub claim.");

var oneLoginUser = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == sub);
oneLoginUser.VerifiedOn = clock.UtcNow;
oneLoginUser.VerificationRoute = OneLoginUserVerificationRoute.OneLogin;
oneLoginUser.VerifiedNames = verifiedNames;
oneLoginUser.VerifiedDatesOfBirth = verifiedDatesOfBirth;
oneLoginUser.SetVerified(clock.UtcNow, OneLoginUserVerificationRoute.OneLogin, verifiedByApplicationUserId: null, verifiedNames, verifiedDatesOfBirth);
oneLoginUser.LastCoreIdentityVc = coreIdentityClaimVc;

string? trn = null;
Expand All @@ -167,11 +166,9 @@ public async Task OnUserVerifiedCore(

if (result.MatchRoute is not null)
{
oneLoginUser.PersonId = result.PersonId;
oneLoginUser.FirstSignIn = clock.UtcNow;
oneLoginUser.LastSignIn = clock.UtcNow;
oneLoginUser.MatchRoute = result.MatchRoute.Value;
oneLoginUser.MatchedAttributes = result.MatchedAttributes!.ToArray();
oneLoginUser.SetMatched(result.PersonId, result.MatchRoute.Value, result.MatchedAttributes!.ToArray());
trn = result.Trn;
}
}
Expand Down Expand Up @@ -319,11 +316,9 @@ public async Task<bool> TryMatchToTeachingRecord(JourneyInstance<SignInJourneySt
var subject = state.OneLoginAuthenticationTicket.Principal.FindFirstValue("sub") ?? throw new InvalidOperationException("No sub claim.");

var oneLoginUser = await dbContext.OneLoginUsers.SingleAsync(o => o.Subject == subject);
oneLoginUser.PersonId = matchedPersonId;
oneLoginUser.FirstSignIn = clock.UtcNow;
oneLoginUser.LastSignIn = clock.UtcNow;
oneLoginUser.MatchRoute = OneLoginUserMatchRoute.Interactive;
oneLoginUser.MatchedAttributes = matchedAttributes.ToArray();
oneLoginUser.SetMatched(matchedPersonId, OneLoginUserMatchRoute.Interactive, matchedAttributes.ToArray());
await dbContext.SaveChangesAsync();

await journeyInstance.UpdateStateAsync(state => Complete(state, matchedTrn));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public void Configure(EntityTypeBuilder<OneLoginUser> builder)
new ValueComparer<KeyValuePair<OneLoginUserMatchedAttribute, string>[]>(
(a, b) => a == b, // Reference equality is fine here; we'll always replace the entire collection
v => v.GetHashCode()));
builder.HasOne<ApplicationUser>().WithMany().HasForeignKey(o => o.VerifiedByApplicationUserId);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;

namespace TeachingRecordSystem.Core.DataStore.Postgres.Mappings;

public class TrnRequestMetadataMapping : IEntityTypeConfiguration<TrnRequestMetadata>
{
public void Configure(EntityTypeBuilder<TrnRequestMetadata> builder)
{
builder.HasKey(r => new { r.ApplicationUserId, r.RequestId });
builder.Property(r => r.RequestId).IsRequired().HasMaxLength(TrnRequest.RequestIdMaxLength);
builder.Property(o => o.VerifiedOneLoginUserSubject).HasMaxLength(255);
}
}
Loading

0 comments on commit 5617151

Please sign in to comment.