diff --git a/src/Application.Interfaces/Audits.Designer.cs b/src/Application.Interfaces/Audits.Designer.cs index f262f831..e758f46a 100644 --- a/src/Application.Interfaces/Audits.Designer.cs +++ b/src/Application.Interfaces/Audits.Designer.cs @@ -59,6 +59,33 @@ internal Audits() { } } + /// + /// Looks up a localized string similar to CSRFProtection.Failed. + /// + public static string CSRFMiddleware_CSRFProtection_Failed { + get { + return ResourceManager.GetString("CSRFMiddleware_CSRFProtection_Failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EndUser.PlatformFeatureAssigned. + /// + public static string EndUserApplication_PlatformFeatureAssigned { + get { + return ResourceManager.GetString("EndUserApplication_PlatformFeatureAssigned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EndUser.PlatformFeatureUnassigned. + /// + public static string EndUserApplication_PlatformFeatureUnassigned { + get { + return ResourceManager.GetString("EndUserApplication_PlatformFeatureUnassigned", resourceCulture); + } + } + /// /// Looks up a localized string similar to EndUser.PlatformRolesAssigned. /// @@ -77,6 +104,24 @@ public static string EndUserApplication_PlatformRolesUnassigned { } } + /// + /// Looks up a localized string similar to EndUser.TenantFeatureAssigned. + /// + public static string EndUserApplication_TenantFeatureAssigned { + get { + return ResourceManager.GetString("EndUserApplication_TenantFeatureAssigned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EndUser.TenantFeatureUnassigned. + /// + public static string EndUserApplication_TenantFeatureUnassigned { + get { + return ResourceManager.GetString("EndUserApplication_TenantFeatureUnassigned", resourceCulture); + } + } + /// /// Looks up a localized string similar to EndUser.TenantRolesAssigned. /// diff --git a/src/Application.Interfaces/Audits.resx b/src/Application.Interfaces/Audits.resx index 47da3f3a..eb42dd23 100644 --- a/src/Application.Interfaces/Audits.resx +++ b/src/Application.Interfaces/Audits.resx @@ -63,4 +63,19 @@ Authentication.Passed + + CSRFProtection.Failed + + + EndUser.PlatformFeatureAssigned + + + EndUser.PlatformFeatureUnassigned + + + EndUser.TenantFeatureAssigned + + + EndUser.TenantFeatureUnassigned + \ No newline at end of file diff --git a/src/Application.Interfaces/UsageConstants.cs b/src/Application.Interfaces/UsageConstants.cs index e370ad96..504032eb 100644 --- a/src/Application.Interfaces/UsageConstants.cs +++ b/src/Application.Interfaces/UsageConstants.cs @@ -14,7 +14,11 @@ public static class Components public static class Properties { public const string AuditCode = "Code"; + public const string AvatarUrl = "AvatarUrl"; public const string CallId = "CallId"; + public const string CarMake = "Make"; + public const string CarModel = "Model"; + public const string CarYear = "Year"; public const string Component = "Component"; public const string Duration = "Duration"; public const string EmailAddress = "EmailAddress"; @@ -26,12 +30,15 @@ public static class Properties public const string Id = "ResourceId"; public const string IpAddress = "IpAddress"; public const string MetricEventName = "Metric"; + public const string Name = "Name"; public const string Path = "Path"; + public const string UserIdOverride = "UserIdOverride"; public const string ReferredBy = "ReferredBy"; public const string ResourceId = "ResourceId"; public const string Started = "Started"; public const string TenantId = "TenantId"; public const string Timestamp = "Timestamp"; + public const string Timezone = "Timezone"; public const string UsedById = "UserId"; public const string UserAgent = "UserAgent"; } @@ -40,19 +47,29 @@ public static class Events { public static class UsageScenarios { - public const string Audit = "Audited"; - public const string BookingCancelled = "Booking Cancelled"; - public const string BookingCreated = "Booking Created"; - public const string GuestInvited = "User Guest Invited"; - public const string MachineRegistered = "Machine Registered"; - public const string Measurement = "Measured"; - public const string PersonRegistrationConfirmed = "User Registered"; - public const string PersonRegistrationCreated = "User Registration Created"; - public const string PersonReRegistered = "User Registration ReAttempted"; - public const string UserExtendedLogin = "User Extended Login"; - public const string UserLogin = "User Login"; - public const string UserLogout = "User Logout"; - public const string UserPasswordReset = "User Password Reset"; + public static class Core + { + public const string BookingCancelled = "Booking Cancelled"; + public const string BookingCreated = "Booking Created"; + public const string CarRegistered = "Car Registered"; + } + + public static class Generic + { + public const string Audit = "Audited"; + public const string GuestInvited = "User Guest Invited"; + public const string MachineRegistered = "Machine Registered"; + public const string Measurement = "Measured"; + public const string PersonRegistrationConfirmed = "User Registered"; + public const string PersonRegistrationCreated = "User Registration Created"; + public const string PersonReRegistered = "User Registration ReAttempted"; + public const string UserExtendedLogin = "User Extended Login"; + public const string UserLogin = "User Login"; + public const string UserLogout = "User Logout"; + public const string UserPasswordForgotten = "User Password Forgotten"; + public const string UserPasswordReset = "User Password Reset"; + public const string UserProfileChanged = "User Profile Updated"; + } } public static class Web diff --git a/src/BookingsApplication/BookingsApplication.cs b/src/BookingsApplication/BookingsApplication.cs index b656e6b1..d1674311 100644 --- a/src/BookingsApplication/BookingsApplication.cs +++ b/src/BookingsApplication/BookingsApplication.cs @@ -57,7 +57,7 @@ public async Task> CancelBookingAsync(ICallerContext caller, strin } _recorder.TraceInformation(caller.ToCall(), "Booking {Id} was cancelled", booking.Id); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.BookingCancelled, + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Core.BookingCancelled, new Dictionary { { UsageConstants.Properties.Id, booking.Id }, @@ -112,7 +112,7 @@ public async Task> MakeBookingAsync(ICallerContext caller booking = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Booking {Id} was created", booking.Id); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.BookingCreated, + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Core.BookingCreated, new Dictionary { { UsageConstants.Properties.Id, booking.Id }, diff --git a/src/CarsApplication/CarsApplication.cs b/src/CarsApplication/CarsApplication.cs index 45d22d3a..7aa0eda3 100644 --- a/src/CarsApplication/CarsApplication.cs +++ b/src/CarsApplication/CarsApplication.cs @@ -153,6 +153,14 @@ public async Task> RegisterCarAsync(ICallerContext caller, st return saved.Match>(c => { _recorder.TraceInformation(caller.ToCall(), "Car {Id} was registered", c.Value.Id); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Core.CarRegistered, + new Dictionary + { + { UsageConstants.Properties.Id, car.Id }, + { UsageConstants.Properties.CarMake, car.Manufacturer.Value.Make.Text }, + { UsageConstants.Properties.CarModel, car.Manufacturer.Value.Model.Text }, + { UsageConstants.Properties.CarYear, car.Manufacturer.Value.Year.Number } + }); return c.Value.ToCar(); }, error => error); } diff --git a/src/EndUsersApplication/EndUsersApplication.cs b/src/EndUsersApplication/EndUsersApplication.cs index 5ef0f328..521025ce 100644 --- a/src/EndUsersApplication/EndUsersApplication.cs +++ b/src/EndUsersApplication/EndUsersApplication.cs @@ -151,7 +151,7 @@ public async Task> RegisterMachineAsync(ICaller machine = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Registered machine: {Id}", machine.Id); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.MachineRegistered); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.MachineRegistered); if (caller.IsAuthenticated) { @@ -290,7 +290,7 @@ public async Task> RegisterPersonAsync(ICallerC _recorder.TraceInformation(caller.ToCall(), "Attempted re-registration of user: {Id}, with email {EmailAddress}", unregisteredUser.Id, email); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.PersonReRegistered, + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.PersonReRegistered, new Dictionary { { UsageConstants.Properties.Id, unregisteredUser.Id }, @@ -339,7 +339,7 @@ public async Task> RegisterPersonAsync(ICallerC _recorder.AuditAgainst(caller.ToCall(), person.Id, Audits.EndUsersApplication_User_Registered_TermsAccepted, "EndUser {Id} accepted their terms and conditions", person.Id); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.PersonRegistrationCreated); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.PersonRegistrationCreated); var defaultOrganizationId = person.DefaultMembership.OrganizationId; var serviceCaller = Caller.CreateAsMaintenance(caller.CallId); diff --git a/src/EndUsersApplication/InvitationsApplication.cs b/src/EndUsersApplication/InvitationsApplication.cs index 0b7a3063..4f1f6ac1 100644 --- a/src/EndUsersApplication/InvitationsApplication.cs +++ b/src/EndUsersApplication/InvitationsApplication.cs @@ -54,7 +54,7 @@ public async Task> InviteGuestAsync(ICallerContext cal invitee = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Guest {Id} was invited", invitee.Id); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.GuestInvited, + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.GuestInvited, new Dictionary { { nameof(EndUserRoot.Id), invitee.Id }, @@ -103,7 +103,7 @@ public async Task> ResendGuestInvitationAsync(ICallerContext calle } _recorder.TraceInformation(caller.ToCall(), "Guest {Id} was re-invited", invitee.Id); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.GuestInvited, + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.GuestInvited, new Dictionary { { nameof(EndUserRoot.Id), invitee.Id }, diff --git a/src/IdentityApplication/PasswordCredentialsApplication.cs b/src/IdentityApplication/PasswordCredentialsApplication.cs index 85a358ae..6fdd9d48 100644 --- a/src/IdentityApplication/PasswordCredentialsApplication.cs +++ b/src/IdentityApplication/PasswordCredentialsApplication.cs @@ -233,6 +233,11 @@ await _notificationsService.NotifyPasswordResetUnknownUserCourtesyAsync(caller, credentials = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Password reset initiated for {Id}", credentials.UserId); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserPasswordForgotten, + new Dictionary + { + { nameof(PasswordCredential.Id), credentials.UserId } + }); return Result.Ok; } @@ -350,7 +355,7 @@ public async Task> CompletePasswordResetAsync(ICallerContext calle credentials = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Password was reset for {Id}", credentials.UserId); - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.UserPasswordReset, + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserPasswordReset, new Dictionary { { nameof(credentials.Id), credentials.UserId } @@ -414,7 +419,7 @@ public async Task> ConfirmPersonRegistrationAsync(ICallerContext c _recorder.TraceInformation(caller.ToCall(), "Password credentials for {UserId} have been verified", credential.UserId); _recorder.TrackUsage(caller.ToCall(), - UsageConstants.Events.UsageScenarios.PersonRegistrationConfirmed, + UsageConstants.Events.UsageScenarios.Generic.PersonRegistrationConfirmed, new Dictionary { { nameof(credential.Id), credential.UserId } diff --git a/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs b/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs index 7d08dbbc..2525fcd3 100644 --- a/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs +++ b/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs @@ -164,7 +164,7 @@ public void AuditAgainst(ICallContext? context, string againstId, string auditCo AugmentMessageTemplateAndArguments(context, $"Audit: {auditCode}, against {againstId}, {messageTemplate}", templateArgs); TraceInformation(safeContext, augmentedMessageTemplate, augmentedArguments); - TrackUsageFor(safeContext, againstId, UsageConstants.Events.UsageScenarios.Audit, + TrackUsageFor(safeContext, againstId, UsageConstants.Events.UsageScenarios.Generic.Audit, new Dictionary { { UsageConstants.Properties.UsedById, againstId }, @@ -202,7 +202,7 @@ public void Measure(ICallContext? context, string eventName, Dictionary(); usageContext.Add(UsageConstants.Properties.MetricEventName, eventName.ToLowerInvariant()); - TrackUsage(safeContext, UsageConstants.Events.UsageScenarios.Measurement, usageContext); + TrackUsage(safeContext, UsageConstants.Events.UsageScenarios.Generic.Measurement, usageContext); _metricsReporter.Measure(safeContext, eventName, additional ?? new Dictionary()); } diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs index 8ef79305..16b623db 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/Pipeline/CSRFMiddlewareSpec.cs @@ -1,6 +1,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Security.Claims; +using Application.Interfaces; using Application.Interfaces.Services; using Common; using Common.Extensions; @@ -24,6 +25,7 @@ public class CSRFMiddlewareSpec private readonly CSRFMiddleware _middleware; private readonly Mock _next; private readonly ServiceProvider _serviceProvider; + private readonly Mock _callerContextFactory; public CSRFMiddlewareSpec() { @@ -40,6 +42,9 @@ public CSRFMiddlewareSpec() var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(new LoggerFactory()); _serviceProvider = serviceCollection.BuildServiceProvider(); + _callerContextFactory = new Mock(); + _callerContextFactory.Setup(c => c.Create()) + .Returns(Mock.Of(cc => cc.CallerId == "auserid")); _middleware = new CSRFMiddleware(_next.Object, recorder.Object, _hostSettings.Object, _csrfService.Object); } @@ -52,7 +57,7 @@ public async Task WhenInvokeAsyncAndIsIgnoredMethod_ThenContinuesPipeline() Request = { Method = HttpMethods.Get } }; - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); _next.Verify(n => n.Invoke(context)); _csrfService.Verify( @@ -66,7 +71,7 @@ public async Task WhenInvokeAsyncAndMissingHostName_ThenRespondsWithAProblem() var context = SetupContext(); _hostSettings.Setup(s => s.GetWebsiteHostBaseUrl()).Returns("notauri"); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.InternalServerError, Resources.CSRFMiddleware_InvalidHostName.Format("notauri")); @@ -78,7 +83,7 @@ public async Task WhenInvokeAsyncAndMissingCookie_ThenRespondsWithAProblem() { var context = SetupContext(); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_MissingCSRFCookieValue); @@ -92,7 +97,7 @@ public async Task WhenInvokeAsyncAndMissingHeader_ThenRespondsWithAProblem() context.Request.Cookies = SetupCookies(new Dictionary { { CSRFConstants.Cookies.AntiCSRF, "ananticsrfcookie" } }); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_MissingCSRFHeaderValue); @@ -110,7 +115,7 @@ public async Task WhenInvokeAsyncAndAuthTokenIsInvalid_ThenRespondsWithAProblem( }); context.Request.Headers.Append(CSRFConstants.Headers.AntiCSRF, new StringValues("ananticsrfheader")); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_InvalidToken); @@ -131,7 +136,7 @@ public async Task WhenInvokeAsyncAndTokenNotContainUserIdClaim_ThenRespondsWithA }); context.Request.Headers.Append(CSRFConstants.Headers.AntiCSRF, new StringValues("ananticsrfheader")); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_InvalidToken); @@ -152,7 +157,7 @@ public async Task WhenInvokeAsyncAndTokensNotVerifiedForNoUser_ThenRespondsWithA It.IsAny>())) .Returns(false); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_InvalidSignature.Format(nameof(Optional.None))); @@ -181,7 +186,7 @@ public async Task WhenInvokeAsyncAndTokensNotVerifiedForUser_ThenRespondsWithAPr It.IsAny>())) .Returns(false); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_InvalidSignature.Format("auserid")); @@ -212,7 +217,7 @@ public async Task WhenInvokeAsyncAndTokensIsVerifiedButNoOriginAndNoReferer_Then It.IsAny>())) .Returns(true); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_MissingOriginAndReferer); @@ -243,7 +248,7 @@ public async Task WhenInvokeAsyncAndTokensIsVerifiedButOriginNotHost_ThenRespond It.IsAny>())) .Returns(true); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_OriginMismatched); @@ -274,7 +279,7 @@ public async Task WhenInvokeAsyncAndTokensIsVerifiedButRefererNotHost_ThenRespon It.IsAny>())) .Returns(true); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().BeAProblem(HttpStatusCode.Forbidden, Resources.CSRFMiddleware_RefererMismatched); @@ -305,7 +310,7 @@ public async Task WhenInvokeAsyncAndTokensIsVerifiedAndOriginIsHostForUser_ThenC It.IsAny>())) .Returns(true); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().NotBeAProblem(); _csrfService.Setup(crs => crs.VerifyTokens("ananticsrfheader", "ananticsrfcookie", "auserid")) @@ -335,7 +340,7 @@ public async Task WhenInvokeAsyncAndTokensIsVerifiedAndRefererIsHostForUser_Then It.IsAny>())) .Returns(true); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().NotBeAProblem(); _csrfService.Setup(crs => crs.VerifyTokens("ananticsrfheader", "ananticsrfcookie", "auserid")) @@ -358,7 +363,7 @@ public async Task WhenInvokeAsyncAndTokensIsVerifiedAndOriginIsHostForNoUser_The It.IsAny>())) .Returns(true); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().NotBeAProblem(); _csrfService.Setup(crs => crs.VerifyTokens("ananticsrfheader", "ananticsrfcookie", Optional.None)) @@ -381,7 +386,7 @@ public async Task WhenInvokeAsyncAndTokensIsVerifiedAndRefererIsHostForNoUser_Th It.IsAny>())) .Returns(true); - await _middleware.InvokeAsync(context); + await _middleware.InvokeAsync(context, _callerContextFactory.Object); context.Response.Should().NotBeAProblem(); _csrfService.Setup(crs => crs.VerifyTokens("ananticsrfheader", "ananticsrfcookie", Optional.None)) diff --git a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs index 5f0afddd..eea9072d 100644 --- a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs +++ b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFMiddleware.cs @@ -1,6 +1,9 @@ +using Application.Common.Extensions; +using Application.Interfaces; using Application.Interfaces.Services; using Common; using Common.Extensions; +using Infrastructure.Interfaces; using Infrastructure.Web.Api.Common; using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Hosting.Common.Extensions; @@ -37,8 +40,9 @@ public CSRFMiddleware(RequestDelegate next, IRecorder recorder, IHostSettings ho _csrfService = csrfService; } - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext context, ICallerContextFactory callerContextFactory) { + var caller = callerContextFactory.Create(); var request = context.Request; if (IgnoredMethods.Contains(request.Method)) { @@ -51,6 +55,8 @@ public async Task InvokeAsync(HttpContext context) { var httpError = result.Error.ToHttpError(); var details = Results.Problem(statusCode: (int)httpError.Code, detail: httpError.Message); + _recorder.Audit(caller.ToCall(), Audits.CSRFMiddleware_CSRFProtection_Failed, + "User {Id} failed CSRF protection", caller.CallerId); await details .ExecuteAsync(context); return; diff --git a/src/Infrastructure.Web.Website.UnitTests/Application/AuthenticationApplicationSpec.cs b/src/Infrastructure.Web.Website.UnitTests/Application/AuthenticationApplicationSpec.cs index 69d3684e..97a1a39b 100644 --- a/src/Infrastructure.Web.Website.UnitTests/Application/AuthenticationApplicationSpec.cs +++ b/src/Infrastructure.Web.Website.UnitTests/Application/AuthenticationApplicationSpec.cs @@ -37,7 +37,7 @@ public async Task WhenLogout_ThenLogsOut() result.Should().BeSuccess(); _recorder.Verify(rec => - rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.UserLogout, null)); + rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.Generic.UserLogout, null)); } [Fact] @@ -78,7 +78,7 @@ public async Task WhenAuthenticateWithCredentials_ThenAuthenticates() && req.Password == "apassword" ), null, It.IsAny())); _recorder.Verify(rec => - rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.UserLogin, null)); + rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.Generic.UserLogin, null)); } [Fact] @@ -118,7 +118,7 @@ public async Task WhenAuthenticateWithSingleSignOn_ThenAuthenticates() req.AuthCode == "anauthcode" ), null, It.IsAny())); _recorder.Verify(rec => - rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.UserLogin, null)); + rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.Generic.UserLogin, null)); } [Fact] @@ -172,6 +172,7 @@ public async Task WhenRefreshTokenCookieExists_ThenRefreshesAndSetsCookie() req.RefreshToken == "arefreshtoken" ), null, It.IsAny())); _recorder.Verify(rec => - rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.UserExtendedLogin, null)); + rec.TrackUsage(It.IsAny(), UsageConstants.Events.UsageScenarios.Generic.UserExtendedLogin, + null)); } } \ No newline at end of file diff --git a/src/UserProfilesApplication/UserProfilesApplication.cs b/src/UserProfilesApplication/UserProfilesApplication.cs index 7c014f86..b10759bc 100644 --- a/src/UserProfilesApplication/UserProfilesApplication.cs +++ b/src/UserProfilesApplication/UserProfilesApplication.cs @@ -63,6 +63,8 @@ public async Task> ChangeProfileAvatarAsync(ICallerCo profile = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Profile {Id} avatar was added for user {UserId}", profile.Id, userId); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserProfileChanged, + profile.ToUsageEvent(caller)); return profile.ToProfile(); } @@ -103,6 +105,8 @@ public async Task> DeleteProfileAvatarAsync(ICallerCo profile = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Profile {Id} avatar was deleted for user {UserId}", profile.Id, userId); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserProfileChanged, + profile.ToUsageEvent(caller)); return profile.ToProfile(); } @@ -293,6 +297,8 @@ public async Task> ChangeProfileAsync(ICallerContext profile = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Profile {Id} was updated for user {UserId}", profile.Id, profile.UserId); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserProfileChanged, + profile.ToUsageEvent(caller)); return profile.ToProfile(); } @@ -348,6 +354,8 @@ public async Task> ChangeContactAddressAsync(ICallerC profile = saved.Value; _recorder.TraceInformation(caller.ToCall(), "Profile {Id} contact address was updated for user {UserId}", profile.Id, userId); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserProfileChanged, + profile.ToUsageEvent(caller)); return profile.ToProfile(); } @@ -436,6 +444,27 @@ public static UserProfile ToProfile(this UserProfileRoot profile) }; } + public static Dictionary ToUsageEvent(this UserProfileRoot profile, ICallerContext caller) + { + var context = new Dictionary + { + [UsageConstants.Properties.UserIdOverride] = profile.UserId, + [UsageConstants.Properties.Name] = profile.Name.ToName(), + [UsageConstants.Properties.Timezone] = profile.Timezone.Code.ToString() + }; + if (profile.EmailAddress.HasValue) + { + context[UsageConstants.Properties.EmailAddress] = profile.EmailAddress.Value.Address; + } + + if (profile.Avatar.HasValue) + { + context[UsageConstants.Properties.AvatarUrl] = profile.Avatar.Value.Url; + } + + return context; + } + private static ProfileAddress ToAddress(this Address address) { var dto = address.Convert(); diff --git a/src/WebsiteHost/Application/AuthenticationApplication.cs b/src/WebsiteHost/Application/AuthenticationApplication.cs index 360d1f50..36fb334d 100644 --- a/src/WebsiteHost/Application/AuthenticationApplication.cs +++ b/src/WebsiteHost/Application/AuthenticationApplication.cs @@ -50,14 +50,14 @@ public async Task> AuthenticateAsync(ICallerCo return authenticated.Error.ToError(); } - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.UserLogin); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserLogin); return authenticated.Value.ToTokens(); } public Task> LogoutAsync(ICallerContext caller, CancellationToken cancellationToken) { - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.UserLogout); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserLogout); return Task.FromResult(Result.Ok); } @@ -79,7 +79,7 @@ public async Task> RefreshTokenAsync(ICallerCo return refreshed.Error.ToError(); } - _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.UserExtendedLogin); + _recorder.TrackUsage(caller.ToCall(), UsageConstants.Events.UsageScenarios.Generic.UserExtendedLogin); return refreshed.Value.ToTokens(); }