Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

Reimplemented API keys. #63

Merged
merged 10 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions lib/Logitar.Identity.Core/ApiKeys/ApiKeyId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Logitar.EventSourcing;

namespace Logitar.Identity.Core.ApiKeys;

/// <summary>
/// Represents the identifier of an API key.
/// </summary>
public readonly struct ApiKeyId
{
/// <summary>
/// Gets the identifier of the event stream.
/// </summary>
public StreamId StreamId { get; }
/// <summary>
/// Gets the value of the identifier.
/// </summary>
public string Value => StreamId.Value;

/// <summary>
/// Gets the tenant identifier.
/// </summary>
public TenantId? TenantId { get; }
/// <summary>
/// Gets the entity identifier.
/// </summary>
public EntityId EntityId { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyId"/> struct.
/// </summary>
/// <param name="tenantId">The tenant identifier.</param>
/// <param name="entityId">The entity identifier.</param>
public ApiKeyId(TenantId? tenantId, Guid entityId) : this(tenantId, Convert.ToBase64String(entityId.ToByteArray()).ToUriSafeBase64())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyId"/> struct.
/// </summary>
/// <param name="tenantId">The tenant identifier.</param>
/// <param name="entityId">The entity identifier.</param>
public ApiKeyId(TenantId? tenantId, string entityId)
{
TenantId = tenantId;
EntityId = new(entityId);
StreamId = new(tenantId.HasValue ? $"{tenantId}:{entityId}" : entityId);
}
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyId"/> struct.
/// </summary>
/// <param name="streamId">A stream identifier.</param>
public ApiKeyId(StreamId streamId)
{
StreamId = streamId;
}

/// <summary>
/// Randomly generates a new API key identifier.
/// </summary>
/// <param name="tenantId">The tenant identifier.</param>
/// <returns>The generated identifier.</returns>
public static ApiKeyId NewId(TenantId? tenantId = null) => new(tenantId, Guid.NewGuid());

/// <summary>
/// Returns a value indicating whether or not the specified identifiers are equal.
/// </summary>
/// <param name="left">The first identifier to compare.</param>
/// <param name="right">The other identifier to compare.</param>
/// <returns>True if the identifiers are equal.</returns>
public static bool operator ==(ApiKeyId left, ApiKeyId right) => left.Equals(right);
/// <summary>
/// Returns a value indicating whether or not the specified identifiers are different.
/// </summary>
/// <param name="left">The first identifier to compare.</param>
/// <param name="right">The other identifier to compare.</param>
/// <returns>True if the identifiers are different.</returns>
public static bool operator !=(ApiKeyId left, ApiKeyId right) => !left.Equals(right);

/// <summary>
/// Returns a value indicating whether or not the specified object is equal to the identifier.
/// </summary>
/// <param name="obj">The object to be compared to.</param>
/// <returns>True if the object is equal to the identifier.</returns>
public override bool Equals([NotNullWhen(true)] object? obj) => obj is ApiKeyId id && id.Value == Value;
/// <summary>
/// Returns the hash code of the current identifier.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode() => Value.GetHashCode();
/// <summary>
/// Returns a string representation of the identifier.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString() => Value;
}
39 changes: 39 additions & 0 deletions lib/Logitar.Identity.Core/ApiKeys/ApiKeyIsExpiredException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Logitar.Identity.Core.ApiKeys;

/// <summary>
/// The exception raised when an expired API key is authenticated.
/// </summary>
public class ApiKeyIsExpiredException : InvalidCredentialsException
{
/// <summary>
/// A generic error message for this exception.
/// </summary>
private const string ErrorMessage = "The specified API key is expired.";

/// <summary>
/// Gets the identifier of the expired API key.
/// </summary>
public string ApiKeyId
{
get => (string)Data[nameof(ApiKeyId)]!;
private set => Data[nameof(ApiKeyId)] = value;
}

/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyIsExpiredException"/> class.
/// </summary>
/// <param name="apiKey">The API key that is expired.</param>
public ApiKeyIsExpiredException(ApiKey apiKey) : base(BuildMessage(apiKey))
{
ApiKeyId = apiKey.Id.Value;
}

/// <summary>
/// Builds the exception message.
/// </summary>
/// <param name="apiKey">The API key that is expired.</param>
/// <returns>The exception message.</returns>
private static string BuildMessage(ApiKey apiKey) => new ErrorMessageBuilder(ErrorMessage)
.AddData(nameof(ApiKeyId), apiKey.Id)
.Build();
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Identity.Domain.ApiKeys.Events;
namespace Logitar.Identity.Core.ApiKeys.Events;

/// <summary>
/// The event raised when an API key is authenticated.
/// </summary>
public class ApiKeyAuthenticatedEvent : DomainEvent, INotification;
public record ApiKeyAuthenticated : DomainEvent, INotification;
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
using Logitar.EventSourcing;
using Logitar.Identity.Domain.Passwords;
using Logitar.Identity.Domain.Shared;
using Logitar.Identity.Core.Passwords;
using MediatR;

namespace Logitar.Identity.Domain.ApiKeys.Events;
namespace Logitar.Identity.Core.ApiKeys.Events;

/// <summary>
/// The event raised when a new API key is created.
/// </summary>
public class ApiKeyCreatedEvent : DomainEvent, INotification
public record ApiKeyCreated : DomainEvent, INotification
{
/// <summary>
/// Gets the secret of the API key.
/// </summary>
public Password Secret { get; }

/// <summary>
/// Gets the tenant identifier of the API key.
/// Gets the display name of the API key.
/// </summary>
public TenantId? TenantId { get; }
public DisplayName DisplayName { get; }

/// <summary>
/// Gets the display name of the API key.
/// Gets the secret of the API key.
/// </summary>
public DisplayNameUnit DisplayName { get; }
public Password Secret { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyCreatedEvent"/> class.
/// Initializes a new instance of the <see cref="ApiKeyCreated"/> class.
/// </summary>
/// <param name="displayName">The display name of the API key.</param>
/// <param name="secret">The secret of the API key.</param>
/// <param name="tenantId">The tenant identifier of the API key.</param>
public ApiKeyCreatedEvent(DisplayNameUnit displayName, Password secret, TenantId? tenantId)
public ApiKeyCreated(DisplayName displayName, Password secret)
{
DisplayName = displayName;
Secret = secret;
TenantId = tenantId;
}
}
9 changes: 9 additions & 0 deletions lib/Logitar.Identity.Core/ApiKeys/Events/ApiKeyDeleted.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Identity.Core.ApiKeys.Events;

/// <summary>
/// The event raised when an API key is deleted.
/// </summary>
public record ApiKeyDeleted : DomainEvent, IDeleteEvent, INotification;
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
using Logitar.EventSourcing;
using Logitar.Identity.Domain.Roles;
using Logitar.Identity.Core.Roles;
using MediatR;

namespace Logitar.Identity.Domain.ApiKeys.Events;
namespace Logitar.Identity.Core.ApiKeys.Events;

/// <summary>
/// The event raised when a role is added to an API key.
/// </summary>
public class ApiKeyRoleAddedEvent : DomainEvent, INotification
public record ApiKeyRoleAdded : DomainEvent, INotification
{
/// <summary>
/// Gets the role identifier.
/// </summary>
public RoleId RoleId { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyRoleAddedEvent"/> class.
/// Initializes a new instance of the <see cref="ApiKeyRoleAdded"/> class.
/// </summary>
/// <param name="roleId">The role identifier.</param>
public ApiKeyRoleAddedEvent(RoleId roleId)
public ApiKeyRoleAdded(RoleId roleId)
{
RoleId = roleId;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
using Logitar.EventSourcing;
using Logitar.Identity.Domain.Roles;
using Logitar.Identity.Core.Roles;
using MediatR;

namespace Logitar.Identity.Domain.ApiKeys.Events;
namespace Logitar.Identity.Core.ApiKeys.Events;

/// <summary>
/// The event raised when a role is removed from an API key.
/// </summary>
public class ApiKeyRoleRemovedEvent : DomainEvent, INotification
public record ApiKeyRoleRemoved : DomainEvent, INotification
{
/// <summary>
/// Gets the role identifier.
/// </summary>
public RoleId RoleId { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyRoleRemovedEvent"/> class.
/// Initializes a new instance of the <see cref="ApiKeyRoleRemoved"/> class.
/// </summary>
/// <param name="roleId">The role identifier.</param>
public ApiKeyRoleRemovedEvent(RoleId roleId)
public ApiKeyRoleRemoved(RoleId roleId)
{
RoleId = roleId;
}
Expand Down
34 changes: 34 additions & 0 deletions lib/Logitar.Identity.Core/ApiKeys/Events/ApiKeyUpdated.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Identity.Core.ApiKeys.Events;

/// <summary>
/// The event raised when an API key is updated.
/// </summary>
public record ApiKeyUpdated : DomainEvent, INotification
{
/// <summary>
/// Gets or sets the new display name of the API key.
/// </summary>
public DisplayName? DisplayName { get; set; }
/// <summary>
/// Gets or sets the new description of the API key.
/// </summary>
public Change<Description>? Description { get; set; }
/// <summary>
/// Gets or sets the new expiration date and time of the API key.
/// </summary>
public DateTime? ExpiresOn { get; set; }

/// <summary>
/// Gets or sets the custom attribute modifications of the API key.
/// </summary>
public Dictionary<string, string?> CustomAttributes { get; init; } = [];

/// <summary>
/// Gets a value indicating whether or not the API key has been updated.
/// </summary>
[JsonIgnore]
public bool HasChanges => DisplayName != null || Description != null || ExpiresOn != null || CustomAttributes.Count > 0;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Logitar.Identity.Domain.Shared;

namespace Logitar.Identity.Domain.ApiKeys;
namespace Logitar.Identity.Core.ApiKeys;

/// <summary>
/// The exception raised when an API key secret check fails.
Expand All @@ -10,15 +8,15 @@ public class IncorrectApiKeySecretException : InvalidCredentialsException
/// <summary>
/// A generic error message for this exception.
/// </summary>
public new const string ErrorMessage = "The specified secret did not match the API key.";
private const string ErrorMessage = "The specified secret did not match the API key.";

/// <summary>
/// Gets or sets the identifier of the API key.
/// </summary>
public ApiKeyId ApiKeyId
public string ApiKeyId
{
get => new((string)Data[nameof(ApiKeyId)]!);
private set => Data[nameof(ApiKeyId)] = value.Value;
get => (string)Data[nameof(ApiKeyId)]!;
private set => Data[nameof(ApiKeyId)] = value;
}
/// <summary>
/// Gets or sets the attempted secret.
Expand All @@ -34,15 +32,21 @@ public string AttemptedSecret
/// </summary>
/// <param name="apiKey">The API key.</param>
/// <param name="attemptedSecret">The attempted secret.</param>
public IncorrectApiKeySecretException(ApiKeyAggregate apiKey, string attemptedSecret)
public IncorrectApiKeySecretException(ApiKey apiKey, string attemptedSecret)
: base(BuildMessage(apiKey, attemptedSecret))
{
ApiKeyId = apiKey.Id;
ApiKeyId = apiKey.Id.Value;
AttemptedSecret = attemptedSecret;
}

private static string BuildMessage(ApiKeyAggregate apiKey, string attemptedSecret) => new ErrorMessageBuilder(ErrorMessage)
.AddData(nameof(ApiKeyId), apiKey.Id.Value)
/// <summary>
/// Builds the exception message.
/// </summary>
/// <param name="apiKey">The API key.</param>
/// <param name="attemptedSecret">The attempted secret.</param>
/// <returns>The exception message.</returns>
private static string BuildMessage(ApiKey apiKey, string attemptedSecret) => new ErrorMessageBuilder(ErrorMessage)
.AddData(nameof(ApiKeyId), apiKey.Id)
.AddData(nameof(AttemptedSecret), attemptedSecret)
.Build();
}
Loading
Loading