Skip to content

Commit

Permalink
Add GetEventLogAsync (#98)
Browse files Browse the repository at this point in the history
* Added event log endpoint and starter test.
  • Loading branch information
jrmccannon authored Dec 21, 2023
1 parent 3acdf33 commit 3b284d6
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/Passwordless/Helpers/PasswordlessSerializerContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ namespace Passwordless.Helpers;
[JsonSerializable(typeof(PasswordlessProblemDetails))]
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(GetEventLogRequest))]
[JsonSerializable(typeof(GetEventLogResponse))]
internal partial class PasswordlessSerializerContext : JsonSerializerContext
{

Expand Down
8 changes: 8 additions & 0 deletions src/Passwordless/IPasswordlessClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,12 @@ Task DeleteCredentialAsync(
byte[] id,
CancellationToken cancellationToken = default
);

/// <summary>
/// Gets a list of events for the application.
/// </summary>
/// <param name="request"><see cref="GetEventLogRequest"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<GetEventLogResponse> GetEventLogAsync(GetEventLogRequest request, CancellationToken cancellationToken = default);
}
20 changes: 20 additions & 0 deletions src/Passwordless/Models/GetEventLogRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;

namespace Passwordless.Models;

/// <summary>
/// Request for getting the event logs for an application.
/// </summary>
public class GetEventLogRequest
{
/// <summary>
/// Page number for retrieving event log records.
/// </summary>
public int PageNumber { get; set; }

/// <summary>
/// This is the max number of results that will be returned. Must be between 1-1000.
/// </summary>
[Range(1, 1000)]
public int? NumberOfResults { get; set; }
}
60 changes: 60 additions & 0 deletions src/Passwordless/Models/GetEventLogResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;

namespace Passwordless.Models;

/// <summary>
/// Response from GetEventLog. Contains list of events for the application.
/// </summary>
public class GetEventLogResponse
{
/// <summary>
/// Name of application the events correspond to.
/// </summary>
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// List of events for the application based on the request pagination parameters. This will always be sorted by PerformedAt in descending order.
/// </summary>
public IEnumerable<ApplicationEvent> Events { get; set; } = new List<ApplicationEvent>();
/// <summary>
/// Total number of events for the application.
/// </summary>
public int TotalEventCount { get; set; }
}

/// <summary>
/// An event that occured using Passwordless library.
/// </summary>
public class ApplicationEvent
{
public Guid Id { get; set; }
/// <summary>
/// When the record was performed. This will be in UTC.
/// </summary>
public DateTime PerformedAt { get; set; }

/// <summary>
/// The type of event <see href="https://github.com/passwordless/passwordless-server/blob/main/src/Common/EventLog/Enums/EventType.cs" />
/// </summary>
public string EventType { get; set; } = string.Empty;

/// <summary>
/// Description of the event
/// </summary>
public string Message { get; set; } = string.Empty;

/// <summary>
/// Severity of the event <see href="https://github.com/passwordless/passwordless-server/blob/main/src/Common/EventLog/Enums/Severity.cs"/>
/// </summary>
public string Severity { get; set; } = string.Empty;

/// <summary>
/// The target of the event. Can be in reference to a user or the application.
/// </summary>
public string Subject { get; set; } = string.Empty;

/// <summary>
/// Last 4 characters of the api key (public/secret) used to perform the event.
/// </summary>
public string ApiKeyId { get; set; } = string.Empty;
}
7 changes: 6 additions & 1 deletion src/Passwordless/PasswordlessClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Threading.Tasks;
using Passwordless.Helpers;
using Passwordless.Models;
using JsonContext = Passwordless.Helpers.PasswordlessSerializerContext;

namespace Passwordless;

Expand Down Expand Up @@ -116,6 +115,12 @@ public async Task<VerifiedUser> VerifyTokenAsync(
))!;
}

/// <inheritdoc />
public async Task<GetEventLogResponse> GetEventLogAsync(GetEventLogRequest request, CancellationToken cancellationToken = default) =>
(await _http.GetFromJsonAsync($"events?pageNumber={request.PageNumber}&numberOfResults={request.NumberOfResults}",
PasswordlessSerializerContext.Default.GetEventLogResponse,
cancellationToken)) ?? new GetEventLogResponse();

/// <inheritdoc />
public async Task<UsersCount> GetUsersCountAsync(CancellationToken cancellationToken = default) =>
(await _http.GetFromJsonAsync(
Expand Down
51 changes: 49 additions & 2 deletions tests/Passwordless.Tests.Infra/TestApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class TestApi : IAsyncDisposable

private string PublicApiUrl => $"http://{_apiContainer.Hostname}:{_apiContainer.GetMappedPublicPort(ApiPort)}";

public static string GetAppName() => $"app{Guid.NewGuid():N}";

public TestApi()
{
_apiContainer = new ContainerBuilder()
Expand Down Expand Up @@ -82,11 +84,13 @@ public async Task InitializeAsync()
}
}

public async Task<PasswordlessOptions> CreateAppAsync()
public Task<PasswordlessOptions> CreateAppAsync() => CreateAppAsync($"app{Guid.NewGuid():N}");

public async Task<PasswordlessOptions> CreateAppAsync(string appName)
{
using var request = new HttpRequestMessage(
HttpMethod.Post,
$"{PublicApiUrl}/admin/apps/app{Guid.NewGuid():N}/create"
$"{PublicApiUrl}/admin/apps/{appName}/create"
);

request.Content = new StringContent(
Expand Down Expand Up @@ -137,6 +141,49 @@ public async Task<IPasswordlessClient> CreateClientAsync()
}).BuildServiceProvider().GetRequiredService<IPasswordlessClient>();
}

public async Task<IPasswordlessClient> CreateClientAsync(string applicationName)
{
var options = await CreateAppAsync(applicationName);

// Initialize using a service container to cover more code paths
return new ServiceCollection().AddPasswordlessSdk(o =>
{
o.ApiUrl = options.ApiUrl;
o.ApiKey = options.ApiKey;
o.ApiSecret = options.ApiSecret;
}).BuildServiceProvider().GetRequiredService<IPasswordlessClient>();
}

public async Task EnableEventLogsAsync(string applicationName)
{
using var request = new HttpRequestMessage(
HttpMethod.Post,
$"{PublicApiUrl}/admin/apps/{applicationName}/features"
);
request.Content = new StringContent(
// lang=json
"""
{
"EventLoggingIsEnabled": true,
"EventLoggingRetentionPeriod": 7,
"MaxUsers": null
}
""",
Encoding.UTF8,
"application/json");

using var response = await _http.SendAsync(request);

if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(
$"Failed to enable event logging. " +
$"Status code: {(int)response.StatusCode}. " +
$"Response body: {await response.Content.ReadAsStringAsync()}."
);
}
}

public string GetLogs()
{
var apiContainerStdOutText = Encoding.UTF8.GetString(
Expand Down
27 changes: 27 additions & 0 deletions tests/Passwordless.Tests/ApplicationEventLogsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading.Tasks;
using FluentAssertions;
using Passwordless.Models;
using Passwordless.Tests.Infra;
using Xunit;
using Xunit.Abstractions;

namespace Passwordless.Tests;

public class ApplicationEventLogsTests(TestApiFixture api, ITestOutputHelper testOutput) : ApiTestBase(api, testOutput)
{
[Fact]
public async Task I_can_view_application_event_logs_when_event_logs_are_enabled()
{
// Arrange
var applicationName = TestApi.GetAppName();
var passwordless = await Api.CreateClientAsync(applicationName);
await Api.EnableEventLogsAsync(applicationName);

// Act
var response = await passwordless.GetEventLogAsync(new GetEventLogRequest { PageNumber = 1, NumberOfResults = 100 });

// Assert
response.Should().NotBeNull();
response.TenantId.Should().Be(applicationName);
}
}

0 comments on commit 3b284d6

Please sign in to comment.