From 56298f66e7f703df747bad51ca564184404d9f30 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Fri, 20 Dec 2024 00:26:53 -0500 Subject: [PATCH] Reimplemented Sessions. (#66) --- .../Logitar.Identity.Core}/LICENSE | 0 .../Logitar.Identity.Core.csproj | 38 ++++++- lib/Logitar.Identity.Core/README.md | 3 + .../Sessions/Events/SessionCreated.cs | 12 +-- .../Sessions/Events/SessionDeleted.cs | 9 ++ .../Sessions/Events/SessionRenewed.cs | 10 +- .../Sessions/Events/SessionSignedOut.cs | 4 +- .../Sessions/Events/SessionUpdated.cs | 6 +- .../IncorrectSessionSecretException.cs | 26 +++-- .../Logitar.Identity.Core/Sessions/Session.cs | 102 +++++++++++------- .../Sessions/SessionId.cs | 94 ++++++++++++++++ .../Sessions/SessionIsNotActiveException.cs | 39 +++++++ .../SessionIsNotPersistentException.cs | 39 +++++++ .../Users/Events/UserSignedIn.cs | 2 +- lib/Logitar.Identity.Core/Users/User.cs | 61 ++++++++++- .../Logitar.Identity.Core}/logitar.png | Bin .../Logitar.Identity.Domain.csproj | 77 ------------- old/src/Logitar.Identity.Domain/README.md | 3 - .../Sessions/Events/SessionDeletedEvent.cs | 18 ---- .../Sessions/SessionId.cs | 77 ------------- .../Sessions/SessionIsNotActiveException.cs | 36 ------- .../SessionIsNotPersistentException.cs | 36 ------- .../Users/UserAggregate.cs | 63 ----------- 23 files changed, 374 insertions(+), 381 deletions(-) rename {old/src/Logitar.Identity.Domain => lib/Logitar.Identity.Core}/LICENSE (100%) create mode 100644 lib/Logitar.Identity.Core/README.md rename old/src/Logitar.Identity.Domain/Sessions/Events/SessionCreatedEvent.cs => lib/Logitar.Identity.Core/Sessions/Events/SessionCreated.cs (70%) create mode 100644 lib/Logitar.Identity.Core/Sessions/Events/SessionDeleted.cs rename old/src/Logitar.Identity.Domain/Sessions/Events/SessionRenewedEvent.cs => lib/Logitar.Identity.Core/Sessions/Events/SessionRenewed.cs (66%) rename old/src/Logitar.Identity.Domain/Sessions/Events/SessionSignedOutEvent.cs => lib/Logitar.Identity.Core/Sessions/Events/SessionSignedOut.cs (54%) rename old/src/Logitar.Identity.Domain/Sessions/Events/SessionUpdatedEvent.cs => lib/Logitar.Identity.Core/Sessions/Events/SessionUpdated.cs (69%) rename {old/src/Logitar.Identity.Domain => lib/Logitar.Identity.Core}/Sessions/IncorrectSessionSecretException.cs (55%) rename old/src/Logitar.Identity.Domain/Sessions/SessionAggregate.cs => lib/Logitar.Identity.Core/Sessions/Session.cs (65%) create mode 100644 lib/Logitar.Identity.Core/Sessions/SessionId.cs create mode 100644 lib/Logitar.Identity.Core/Sessions/SessionIsNotActiveException.cs create mode 100644 lib/Logitar.Identity.Core/Sessions/SessionIsNotPersistentException.cs rename {old/src/Logitar.Identity.Domain => lib/Logitar.Identity.Core}/logitar.png (100%) delete mode 100644 old/src/Logitar.Identity.Domain/Logitar.Identity.Domain.csproj delete mode 100644 old/src/Logitar.Identity.Domain/README.md delete mode 100644 old/src/Logitar.Identity.Domain/Sessions/Events/SessionDeletedEvent.cs delete mode 100644 old/src/Logitar.Identity.Domain/Sessions/SessionId.cs delete mode 100644 old/src/Logitar.Identity.Domain/Sessions/SessionIsNotActiveException.cs delete mode 100644 old/src/Logitar.Identity.Domain/Sessions/SessionIsNotPersistentException.cs delete mode 100644 old/src/Logitar.Identity.Domain/Users/UserAggregate.cs diff --git a/old/src/Logitar.Identity.Domain/LICENSE b/lib/Logitar.Identity.Core/LICENSE similarity index 100% rename from old/src/Logitar.Identity.Domain/LICENSE rename to lib/Logitar.Identity.Core/LICENSE diff --git a/lib/Logitar.Identity.Core/Logitar.Identity.Core.csproj b/lib/Logitar.Identity.Core/Logitar.Identity.Core.csproj index 6c11a4d..6cacda5 100644 --- a/lib/Logitar.Identity.Core/Logitar.Identity.Core.csproj +++ b/lib/Logitar.Identity.Core/Logitar.Identity.Core.csproj @@ -3,8 +3,27 @@ net9.0 enable - - true + true + Logitar.Identity.Core + Francis Pion + Logitar + Logitar.Identity + Exposes domain aggregates, events, entities, value objects, exceptions and services of an Identity system. + © 2024 Logitar All Rights Reserved. + logitar.png + README.md + https://github.com/Logitar/Identity + git + 0.0.0.0 + $(AssemblyVersion) + LICENSE + True + 0.0.0 + en-CA + True + Rewrote the framework to include changes made to EventSourcing. + logitar;net;framework;identity;domain + https://github.com/Logitar/Identity/tree/main/lib/Logitar.Identity.Core @@ -40,4 +59,19 @@ + + + \ + True + + + \ + True + + + \ + True + + + diff --git a/lib/Logitar.Identity.Core/README.md b/lib/Logitar.Identity.Core/README.md new file mode 100644 index 0000000..a9b93b4 --- /dev/null +++ b/lib/Logitar.Identity.Core/README.md @@ -0,0 +1,3 @@ +# Logitar.Identity.Core + +Exposes domain aggregates, events, entities, value objects, exceptions and services of an Identity system. diff --git a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionCreatedEvent.cs b/lib/Logitar.Identity.Core/Sessions/Events/SessionCreated.cs similarity index 70% rename from old/src/Logitar.Identity.Domain/Sessions/Events/SessionCreatedEvent.cs rename to lib/Logitar.Identity.Core/Sessions/Events/SessionCreated.cs index 5a8e9e2..d4d87e2 100644 --- a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionCreatedEvent.cs +++ b/lib/Logitar.Identity.Core/Sessions/Events/SessionCreated.cs @@ -1,14 +1,14 @@ using Logitar.EventSourcing; -using Logitar.Identity.Domain.Passwords; -using Logitar.Identity.Domain.Users; +using Logitar.Identity.Core.Passwords; +using Logitar.Identity.Core.Users; using MediatR; -namespace Logitar.Identity.Domain.Sessions.Events; +namespace Logitar.Identity.Core.Sessions.Events; /// /// The event raised when a new session is created. /// -public class SessionCreatedEvent : DomainEvent, INotification +public record SessionCreated : DomainEvent, INotification { /// /// Gets the identifier of the user owning the session. @@ -20,11 +20,11 @@ public class SessionCreatedEvent : DomainEvent, INotification public Password? Secret { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The secret of the session. /// The identifier of the user owning the session. - public SessionCreatedEvent(Password? secret, UserId userId) + public SessionCreated(Password? secret, UserId userId) { Secret = secret; UserId = userId; diff --git a/lib/Logitar.Identity.Core/Sessions/Events/SessionDeleted.cs b/lib/Logitar.Identity.Core/Sessions/Events/SessionDeleted.cs new file mode 100644 index 0000000..18de4eb --- /dev/null +++ b/lib/Logitar.Identity.Core/Sessions/Events/SessionDeleted.cs @@ -0,0 +1,9 @@ +using Logitar.EventSourcing; +using MediatR; + +namespace Logitar.Identity.Core.Sessions.Events; + +/// +/// The event raised when a session is deleted. +/// +public record SessionDeleted : DomainEvent, IDeleteEvent, INotification; diff --git a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionRenewedEvent.cs b/lib/Logitar.Identity.Core/Sessions/Events/SessionRenewed.cs similarity index 66% rename from old/src/Logitar.Identity.Domain/Sessions/Events/SessionRenewedEvent.cs rename to lib/Logitar.Identity.Core/Sessions/Events/SessionRenewed.cs index b486b76..ccc7ac7 100644 --- a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionRenewedEvent.cs +++ b/lib/Logitar.Identity.Core/Sessions/Events/SessionRenewed.cs @@ -1,13 +1,13 @@ using Logitar.EventSourcing; -using Logitar.Identity.Domain.Passwords; +using Logitar.Identity.Core.Passwords; using MediatR; -namespace Logitar.Identity.Domain.Sessions.Events; +namespace Logitar.Identity.Core.Sessions.Events; /// /// The event raised when a session is renewed. /// -public class SessionRenewedEvent : DomainEvent, INotification +public record SessionRenewed : DomainEvent, INotification { /// /// Gets the new secret of the session. @@ -15,10 +15,10 @@ public class SessionRenewedEvent : DomainEvent, INotification public Password Secret { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The new secret of the session. - public SessionRenewedEvent(Password secret) + public SessionRenewed(Password secret) { Secret = secret; } diff --git a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionSignedOutEvent.cs b/lib/Logitar.Identity.Core/Sessions/Events/SessionSignedOut.cs similarity index 54% rename from old/src/Logitar.Identity.Domain/Sessions/Events/SessionSignedOutEvent.cs rename to lib/Logitar.Identity.Core/Sessions/Events/SessionSignedOut.cs index de9fe2e..795c638 100644 --- a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionSignedOutEvent.cs +++ b/lib/Logitar.Identity.Core/Sessions/Events/SessionSignedOut.cs @@ -1,9 +1,9 @@ using Logitar.EventSourcing; using MediatR; -namespace Logitar.Identity.Domain.Sessions.Events; +namespace Logitar.Identity.Core.Sessions.Events; /// /// The event raised when an active session is signed-out. /// -public class SessionSignedOutEvent : DomainEvent, INotification; +public record SessionSignedOut : DomainEvent, INotification; diff --git a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionUpdatedEvent.cs b/lib/Logitar.Identity.Core/Sessions/Events/SessionUpdated.cs similarity index 69% rename from old/src/Logitar.Identity.Domain/Sessions/Events/SessionUpdatedEvent.cs rename to lib/Logitar.Identity.Core/Sessions/Events/SessionUpdated.cs index 3cf2924..e2c8b2c 100644 --- a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionUpdatedEvent.cs +++ b/lib/Logitar.Identity.Core/Sessions/Events/SessionUpdated.cs @@ -1,17 +1,17 @@ using Logitar.EventSourcing; using MediatR; -namespace Logitar.Identity.Domain.Sessions.Events; +namespace Logitar.Identity.Core.Sessions.Events; /// /// The event raised when an existing session is modified. /// -public class SessionUpdatedEvent : DomainEvent, INotification +public record SessionUpdated : DomainEvent, INotification { /// /// Gets or sets the custom attribute modifications of the session. /// - public Dictionary CustomAttributes { get; init; } = []; + public Dictionary CustomAttributes { get; init; } = []; /// /// Gets a value indicating whether or not the session is being modified. diff --git a/old/src/Logitar.Identity.Domain/Sessions/IncorrectSessionSecretException.cs b/lib/Logitar.Identity.Core/Sessions/IncorrectSessionSecretException.cs similarity index 55% rename from old/src/Logitar.Identity.Domain/Sessions/IncorrectSessionSecretException.cs rename to lib/Logitar.Identity.Core/Sessions/IncorrectSessionSecretException.cs index 0b3c5f3..87fa516 100644 --- a/old/src/Logitar.Identity.Domain/Sessions/IncorrectSessionSecretException.cs +++ b/lib/Logitar.Identity.Core/Sessions/IncorrectSessionSecretException.cs @@ -1,6 +1,4 @@ -using Logitar.Identity.Domain.Shared; - -namespace Logitar.Identity.Domain.Sessions; +namespace Logitar.Identity.Core.Sessions; /// /// The exception raised when a session secret check fails. @@ -10,15 +8,15 @@ public class IncorrectSessionSecretException : InvalidCredentialsException /// /// A generic error message for this exception. /// - public new const string ErrorMessage = "The specified secret did not match the session."; + private const string ErrorMessage = "The specified secret did not match the session."; /// /// Gets or sets the identifier of the session. /// - public SessionId SessionId + public string SessionId { - get => new((string)Data[nameof(SessionId)]!); - private set => Data[nameof(SessionId)] = value.Value; + get => (string)Data[nameof(SessionId)]!; + private set => Data[nameof(SessionId)] = value; } /// /// Gets or sets the attempted secret. @@ -34,15 +32,21 @@ public string AttemptedSecret /// /// The attempted secret. /// The session. - public IncorrectSessionSecretException(SessionAggregate session, string attemptedSecret) + public IncorrectSessionSecretException(Session session, string attemptedSecret) : base(BuildMessage(session, attemptedSecret)) { - SessionId = session.Id; + SessionId = session.Id.Value; AttemptedSecret = attemptedSecret; } - private static string BuildMessage(SessionAggregate session, string attemptedSecret) => new ErrorMessageBuilder(ErrorMessage) + /// + /// Builds the exception message. + /// + /// The attempted secret. + /// The session. + /// The exception message. + private static string BuildMessage(Session session, string attemptedSecret) => new ErrorMessageBuilder(ErrorMessage) .AddData(nameof(AttemptedSecret), attemptedSecret) - .AddData(nameof(SessionId), session.Id.Value) + .AddData(nameof(SessionId), session.Id) .Build(); } diff --git a/old/src/Logitar.Identity.Domain/Sessions/SessionAggregate.cs b/lib/Logitar.Identity.Core/Sessions/Session.cs similarity index 65% rename from old/src/Logitar.Identity.Domain/Sessions/SessionAggregate.cs rename to lib/Logitar.Identity.Core/Sessions/Session.cs index a502d78..69c4de8 100644 --- a/old/src/Logitar.Identity.Domain/Sessions/SessionAggregate.cs +++ b/lib/Logitar.Identity.Core/Sessions/Session.cs @@ -1,24 +1,37 @@ using Logitar.EventSourcing; -using Logitar.Identity.Domain.Passwords; -using Logitar.Identity.Domain.Sessions.Events; -using Logitar.Identity.Domain.Shared; -using Logitar.Identity.Domain.Users; +using Logitar.Identity.Core.Passwords; +using Logitar.Identity.Core.Sessions.Events; +using Logitar.Identity.Core.Users; -namespace Logitar.Identity.Domain.Sessions; +namespace Logitar.Identity.Core.Sessions; /// /// Represents a session in the identity system. A session allows an user to perform actions in a timeframe. /// It can be signed-out to close the timeframe, or renewed to extend the timeframe. /// -public class SessionAggregate : AggregateRoot +public class Session : AggregateRoot { - private SessionUpdatedEvent _updatedEvent = new(); + /// + /// The updated event. + /// + private SessionUpdated _updated = new(); /// /// Gets the identifier of the session. /// - public new SessionId Id => new(base.Id); + public new UserId Id => new(base.Id); + /// + /// Gets the tenant identifier of the session. + /// + public TenantId? TenantId => Id.TenantId; + /// + /// Gets the entity identifier of the session. This identifier is unique within the tenant. + /// + public EntityId? EntityId => Id.EntityId; + /// + /// The identifier of the user owning the session. + /// private UserId? _userId = null; /// /// Gets the identifier of the user owning the session. @@ -26,6 +39,9 @@ public class SessionAggregate : AggregateRoot /// The user identifier has not been initialized yet. public UserId UserId => _userId ?? throw new InvalidOperationException($"The {nameof(UserId)} has not been initialized yet."); + /// + /// The secret of the session. + /// private Password? _secret = null; /// /// Gets a value indicating whether or not the session is persistent. @@ -39,39 +55,47 @@ public class SessionAggregate : AggregateRoot /// public bool IsActive { get; private set; } - private readonly Dictionary _customAttributes = []; + /// + /// The custom attributes of the session. + /// + private readonly Dictionary _customAttributes = []; /// /// Gets the custom attributes of the session. /// - public IReadOnlyDictionary CustomAttributes => _customAttributes.AsReadOnly(); + public IReadOnlyDictionary CustomAttributes => _customAttributes.AsReadOnly(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// DO NOT use this constructor to create a new session. It is only intended to be used by the event sourcing. /// - public SessionAggregate() : base() + public Session() : base() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// DO use this constructor to create a new user. /// /// The user owning the session. /// The secret of the session. /// (Optional) The actor identifier. This parameter should be left null so that it defaults to the user's identifier. /// The identifier of the session. - public SessionAggregate(UserAggregate user, Password? secret = null, ActorId? actorId = null, SessionId? id = null) - : base((id ?? SessionId.NewId()).AggregateId) + /// The user and session tenant identifiers do not match. + public Session(User user, Password? secret = null, ActorId? actorId = null, SessionId? id = null) : base((id ?? SessionId.NewId()).StreamId) { + if (id.HasValue && id.Value.TenantId != user.TenantId) + { + throw new TenantMismatchException(id.Value.TenantId, user.TenantId); + } + actorId ??= new(user.Id.Value); - Raise(new SessionCreatedEvent(secret, user.Id), actorId.Value); + Raise(new SessionCreated(secret, user.Id), actorId.Value); } /// /// Applies the specified event. /// /// The event to apply. - protected virtual void Apply(SessionCreatedEvent @event) + protected virtual void Handle(SessionCreated @event) { _userId = @event.UserId; @@ -84,11 +108,11 @@ protected virtual void Apply(SessionCreatedEvent @event) /// Deletes the session, if it is not already deleted. /// /// The actor identifier. - public void Delete(ActorId actorId = default) + public void Delete(ActorId? actorId = null) { if (!IsDeleted) { - Raise(new SessionDeletedEvent(), actorId); + Raise(new SessionDeleted(), actorId); } } @@ -96,14 +120,11 @@ public void Delete(ActorId actorId = default) /// Removes the specified custom attribute on the session. /// /// The key of the custom attribute. - public void RemoveCustomAttribute(string key) + public void RemoveCustomAttribute(Identifier key) { - key = key.Trim(); - - if (_customAttributes.ContainsKey(key)) + if (_customAttributes.Remove(key)) { - _updatedEvent.CustomAttributes[key] = null; - _customAttributes.Remove(key); + _updated.CustomAttributes[key] = null; } } @@ -132,33 +153,34 @@ public void Renew(string currentSecret, Password newSecret, ActorId? actorId = d } actorId ??= new(UserId.Value); - Raise(new SessionRenewedEvent(newSecret), actorId.Value); + Raise(new SessionRenewed(newSecret), actorId.Value); } /// /// Applies the specified event. /// /// The event to apply. - protected virtual void Apply(SessionRenewedEvent @event) + protected virtual void Handle(SessionRenewed @event) { _secret = @event.Secret; } - private readonly CustomAttributeValidator _customAttributeValidator = new(); /// /// Sets the specified custom attribute on the session. /// /// The key of the custom attribute. /// The value of the custom attribute. - public void SetCustomAttribute(string key, string value) + public void SetCustomAttribute(Identifier key, string value) { - key = key.Trim(); + if (string.IsNullOrWhiteSpace(value)) + { + RemoveCustomAttribute(key); + } value = value.Trim(); - _customAttributeValidator.ValidateAndThrow(key, value); if (!_customAttributes.TryGetValue(key, out string? existingValue) || existingValue != value) { - _updatedEvent.CustomAttributes[key] = value; _customAttributes[key] = value; + _updated.CustomAttributes[key] = value; } } @@ -171,14 +193,14 @@ public void SignOut(ActorId? actorId = default) if (IsActive) { actorId ??= new(UserId.Value); - Raise(new SessionSignedOutEvent(), actorId.Value); + Raise(new SessionSignedOut(), actorId.Value); } } /// /// Applies the specified event. /// /// The event to apply. - protected virtual void Apply(SessionSignedOutEvent _) + protected virtual void Handle(SessionSignedOut _) { IsActive = false; } @@ -187,21 +209,21 @@ protected virtual void Apply(SessionSignedOutEvent _) /// Applies updates on the session. /// /// The actor identifier. - public void Update(ActorId actorId = default) + public void Update(ActorId? actorId = null) { - if (_updatedEvent.HasChanges) + if (_updated.HasChanges) { - Raise(_updatedEvent, actorId, DateTime.Now); - _updatedEvent = new(); + Raise(_updated, actorId, DateTime.Now); + _updated = new(); } } /// /// Applies the specified event. /// /// The event to apply. - protected virtual void Apply(SessionUpdatedEvent @event) + protected virtual void Handle(SessionUpdated @event) { - foreach (KeyValuePair customAttribute in @event.CustomAttributes) + foreach (KeyValuePair customAttribute in @event.CustomAttributes) { if (customAttribute.Value == null) { diff --git a/lib/Logitar.Identity.Core/Sessions/SessionId.cs b/lib/Logitar.Identity.Core/Sessions/SessionId.cs new file mode 100644 index 0000000..85fb717 --- /dev/null +++ b/lib/Logitar.Identity.Core/Sessions/SessionId.cs @@ -0,0 +1,94 @@ +using Logitar.EventSourcing; + +namespace Logitar.Identity.Core.Sessions; + +/// +/// Represents the identifier of a session. +/// +public readonly struct SessionId +{ + /// + /// Gets the identifier of the event stream. + /// + public StreamId StreamId { get; } + /// + /// Gets the value of the identifier. + /// + public string Value => StreamId.Value; + + /// + /// Gets the tenant identifier. + /// + public TenantId? TenantId { get; } + /// + /// Gets the entity identifier. + /// + public EntityId EntityId { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The tenant identifier. + /// The entity identifier. + public SessionId(TenantId? tenantId, Guid entityId) : this(tenantId, Convert.ToBase64String(entityId.ToByteArray()).ToUriSafeBase64()) + { + } + /// + /// Initializes a new instance of the struct. + /// + /// The tenant identifier. + /// The entity identifier. + public SessionId(TenantId? tenantId, string entityId) + { + TenantId = tenantId; + EntityId = new(entityId); + StreamId = new(tenantId.HasValue ? $"{tenantId}:{entityId}" : entityId); + } + /// + /// Initializes a new instance of the struct. + /// + /// A stream identifier. + public SessionId(StreamId streamId) + { + StreamId = streamId; + } + + /// + /// Randomly generates a new session identifier. + /// + /// The tenant identifier. + /// The generated identifier. + public static SessionId NewId(TenantId? tenantId = null) => new(tenantId, Guid.NewGuid()); + + /// + /// Returns a value indicating whether or not the specified identifiers are equal. + /// + /// The first identifier to compare. + /// The other identifier to compare. + /// True if the identifiers are equal. + public static bool operator ==(SessionId left, SessionId right) => left.Equals(right); + /// + /// Returns a value indicating whether or not the specified identifiers are different. + /// + /// The first identifier to compare. + /// The other identifier to compare. + /// True if the identifiers are different. + public static bool operator !=(SessionId left, SessionId right) => !left.Equals(right); + + /// + /// Returns a value indicating whether or not the specified object is equal to the identifier. + /// + /// The object to be compared to. + /// True if the object is equal to the identifier. + public override bool Equals([NotNullWhen(true)] object? obj) => obj is SessionId id && id.Value == Value; + /// + /// Returns the hash code of the current identifier. + /// + /// The hash code. + public override int GetHashCode() => Value.GetHashCode(); + /// + /// Returns a string representation of the identifier. + /// + /// The string representation. + public override string ToString() => Value; +} diff --git a/lib/Logitar.Identity.Core/Sessions/SessionIsNotActiveException.cs b/lib/Logitar.Identity.Core/Sessions/SessionIsNotActiveException.cs new file mode 100644 index 0000000..5693443 --- /dev/null +++ b/lib/Logitar.Identity.Core/Sessions/SessionIsNotActiveException.cs @@ -0,0 +1,39 @@ +namespace Logitar.Identity.Core.Sessions; + +/// +/// The exception raised when an inactive session is renewed. +/// +public class SessionIsNotActiveException : InvalidCredentialsException +{ + /// + /// A generic error message for this exception. + /// + private const string ErrorMessage = "The specified session is not active."; + + /// + /// Gets the identifier of the inactive session. + /// + public string SessionId + { + get => (string)Data[nameof(SessionId)]!; + private set => Data[nameof(SessionId)] = value; + } + + /// + /// Initializes a new instance of the class. + /// + /// The session that is not active. + public SessionIsNotActiveException(Session session) : base(BuildMessage(session)) + { + SessionId = session.Id.Value; + } + + /// + /// Builds the exception message. + /// + /// The session that is not active. + /// The exception message. + private static string BuildMessage(Session session) => new ErrorMessageBuilder(ErrorMessage) + .AddData(nameof(SessionId), session.Id) + .Build(); +} diff --git a/lib/Logitar.Identity.Core/Sessions/SessionIsNotPersistentException.cs b/lib/Logitar.Identity.Core/Sessions/SessionIsNotPersistentException.cs new file mode 100644 index 0000000..973da72 --- /dev/null +++ b/lib/Logitar.Identity.Core/Sessions/SessionIsNotPersistentException.cs @@ -0,0 +1,39 @@ +namespace Logitar.Identity.Core.Sessions; + +/// +/// The exception raised when an ephemeral (not persistent) session is renewed. +/// +public class SessionIsNotPersistentException : InvalidCredentialsException +{ + /// + /// A generic error message for this exception. + /// + private const string ErrorMessage = "The specified session is not persistent."; + + /// + /// Gets the identifier of the ephemeral session. + /// + public string SessionId + { + get => (string)Data[nameof(SessionId)]!; + private set => Data[nameof(SessionId)] = value; + } + + /// + /// Initializes a new instance of the class. + /// + /// The session that is ephemeral. + public SessionIsNotPersistentException(Session session) : base(BuildMessage(session)) + { + SessionId = session.Id.Value; + } + + /// + /// Builds the exception message. + /// + /// The session that is ephemeral. + /// The exception message. + private static string BuildMessage(Session session) => new ErrorMessageBuilder(ErrorMessage) + .AddData(nameof(SessionId), session.Id) + .Build(); +} diff --git a/lib/Logitar.Identity.Core/Users/Events/UserSignedIn.cs b/lib/Logitar.Identity.Core/Users/Events/UserSignedIn.cs index 9051629..d540ff5 100644 --- a/lib/Logitar.Identity.Core/Users/Events/UserSignedIn.cs +++ b/lib/Logitar.Identity.Core/Users/Events/UserSignedIn.cs @@ -12,7 +12,7 @@ public record UserSignedIn : DomainEvent, INotification /// Initializes a new instance of the class. /// /// The date and time when the authentication occurred. - public UserSignedIn(DateTime occurredOn) // TODO(fpion): remove? + public UserSignedIn(DateTime occurredOn) { OccurredOn = occurredOn; } diff --git a/lib/Logitar.Identity.Core/Users/User.cs b/lib/Logitar.Identity.Core/Users/User.cs index cd7eec7..b37213f 100644 --- a/lib/Logitar.Identity.Core/Users/User.cs +++ b/lib/Logitar.Identity.Core/Users/User.cs @@ -1,6 +1,7 @@ using Logitar.EventSourcing; using Logitar.Identity.Core.Passwords; using Logitar.Identity.Core.Roles; +using Logitar.Identity.Core.Sessions; using Logitar.Identity.Core.Users.Events; namespace Logitar.Identity.Core.Users; @@ -679,7 +680,7 @@ public void SetCustomIdentifier(Identifier key, CustomIdentifier value, ActorId? /// Applies the specified event. /// /// The event to apply. - protected virtual void Apply(UserIdentifierChanged @event) + protected virtual void Handle(UserIdentifierChanged @event) { _customIdentifiers[@event.Key] = @event.Value; } @@ -765,6 +766,64 @@ protected virtual void Handle(UserUniqueNameChanged @event) _uniqueName = @event.UniqueName; } + /// + /// Signs-in the user without a password check, opening a new session. + /// + /// The secret of the session. + /// (Optional) The actor identifier. This parameter should be left null so that it defaults to the user's identifier. + /// The identifier of the session. + /// The newly opened session. + /// The password is incorrect. + /// The user has no password. + /// The user is disabled. + public Session SignIn(Password? secret = null, ActorId? actorId = null, SessionId? sessionId = null) + { + return SignIn(password: null, secret, actorId, sessionId); + } + /// + /// Signs-in the user, opening a new session. + /// + /// The password to check. + /// The secret of the session. + /// (Optional) The actor identifier. This parameter should be left null so that it defaults to the user's identifier. + /// The identifier of the session. + /// The newly opened session. + /// The password is incorrect. + /// The user has no password. + /// The user is disabled. + public Session SignIn(string? password, Password? secret = null, ActorId? actorId = null, SessionId? sessionId = null) + { + if (IsDisabled) + { + throw new UserIsDisabledException(this); + } + else if (password != null) + { + if (_password == null) + { + throw new UserHasNoPasswordException(this); + } + else if (!_password.IsMatch(password)) + { + throw new IncorrectUserPasswordException(this, password); + } + } + + actorId ??= new(Id.Value); + Session session = new(this, secret, actorId, sessionId); + Raise(new UserSignedIn(session.CreatedOn), actorId.Value); + + return session; + } + /// + /// Applies the specified event. + /// + /// The event to apply. + protected virtual void Handle(UserSignedIn @event) + { + AuthenticatedOn = @event.OccurredOn; + } + /// /// Applies updates on the user. /// diff --git a/old/src/Logitar.Identity.Domain/logitar.png b/lib/Logitar.Identity.Core/logitar.png similarity index 100% rename from old/src/Logitar.Identity.Domain/logitar.png rename to lib/Logitar.Identity.Core/logitar.png diff --git a/old/src/Logitar.Identity.Domain/Logitar.Identity.Domain.csproj b/old/src/Logitar.Identity.Domain/Logitar.Identity.Domain.csproj deleted file mode 100644 index aa9bf27..0000000 --- a/old/src/Logitar.Identity.Domain/Logitar.Identity.Domain.csproj +++ /dev/null @@ -1,77 +0,0 @@ - - - - net8.0 - enable - enable - true - Logitar.Identity.Domain - Francis Pion - Logitar - Logitar.Identity - Exposes aggregates, events and services from the Identity management platform domain. - © 2024 Logitar All Rights Reserved. - logitar.png - README.md - https://github.com/Logitar/Identity - git - 2.0.0.0 - $(AssemblyVersion) - LICENSE - True - 2.0.0 - en-CA - True - Refactored domain events and aggregates. - logitar;net;framework;identity;domain - https://github.com/Logitar/Identity/tree/main/src/Logitar.Identity.Domain - - - - True - - - - True - - - - - \ - True - - - \ - True - - - \ - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/old/src/Logitar.Identity.Domain/README.md b/old/src/Logitar.Identity.Domain/README.md deleted file mode 100644 index 2110005..0000000 --- a/old/src/Logitar.Identity.Domain/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Logitar.Identity.Domain - -Exposes domain aggregates, events and services of an Identity system. diff --git a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionDeletedEvent.cs b/old/src/Logitar.Identity.Domain/Sessions/Events/SessionDeletedEvent.cs deleted file mode 100644 index e77dbec..0000000 --- a/old/src/Logitar.Identity.Domain/Sessions/Events/SessionDeletedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Logitar.EventSourcing; -using MediatR; - -namespace Logitar.Identity.Domain.Sessions.Events; - -/// -/// The event raised when a session is deleted. -/// -public class SessionDeletedEvent : DomainEvent, INotification -{ - /// - /// Initializes a new instance of the class. - /// - public SessionDeletedEvent() - { - IsDeleted = true; - } -} diff --git a/old/src/Logitar.Identity.Domain/Sessions/SessionId.cs b/old/src/Logitar.Identity.Domain/Sessions/SessionId.cs deleted file mode 100644 index fca0d57..0000000 --- a/old/src/Logitar.Identity.Domain/Sessions/SessionId.cs +++ /dev/null @@ -1,77 +0,0 @@ -using FluentValidation; -using Logitar.EventSourcing; -using Logitar.Identity.Domain.Shared; - -namespace Logitar.Identity.Domain.Sessions; - -/// -/// Represents the identifier of a session. -/// -public record SessionId -{ - /// - /// Gets the aggregate identifier. - /// - public AggregateId AggregateId { get; } - /// - /// Gets the value of the identifier. - /// - public string Value => AggregateId.Value; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier. - /// The name of the property, used for validation. - public SessionId(Guid id, string? propertyName = null) : this(new AggregateId(id), propertyName) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The aggregate identifier. - /// The name of the property, used for validation. - public SessionId(AggregateId aggregateId, string? propertyName = null) - { - new IdValidator(propertyName).ValidateAndThrow(aggregateId.Value); - - AggregateId = aggregateId; - } - - /// - /// Initializes a new instance of the class. - /// - /// The value of the identifier. - /// The name of the property, used for validation. - public SessionId(string value, string? propertyName = null) - { - value = value.Trim(); - new IdValidator(propertyName).ValidateAndThrow(value); - - AggregateId = new(value); - } - - /// - /// Creates a new session identifier. - /// - /// The created identifier. - public static SessionId NewId() => new(AggregateId.NewId()); - - /// - /// Returns null if the input is empty, or a new instance of the class otherwise. - /// - /// The value of the identifier. - /// The name of the property, used for validation. - /// The created instance or null. - public static SessionId? TryCreate(string? value, string? propertyName = null) - { - return string.IsNullOrWhiteSpace(value) ? null : new(value, propertyName); - } - - /// - /// Converts the identifier to a . The conversion will fail if the identifier has not been created from a . - /// - /// The resulting Guid. - public Guid ToGuid() => AggregateId.ToGuid(); -} diff --git a/old/src/Logitar.Identity.Domain/Sessions/SessionIsNotActiveException.cs b/old/src/Logitar.Identity.Domain/Sessions/SessionIsNotActiveException.cs deleted file mode 100644 index e192cc7..0000000 --- a/old/src/Logitar.Identity.Domain/Sessions/SessionIsNotActiveException.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Logitar.Identity.Domain.Shared; - -namespace Logitar.Identity.Domain.Sessions; - -/// -/// The exception raised when an inactive session is renewed. -/// -public class SessionIsNotActiveException : InvalidCredentialsException -{ - /// - /// A generic error message for this exception. - /// - public new const string ErrorMessage = "The specified session is not active."; - - /// - /// Gets the identifier of the inactive session. - /// - public SessionId SessionId - { - get => new((string)Data[nameof(SessionId)]!); - private set => Data[nameof(SessionId)] = value.Value; - } - - /// - /// Initializes a new instance of the class. - /// - /// The session that is not active. - public SessionIsNotActiveException(SessionAggregate session) : base(BuildMessage(session)) - { - SessionId = session.Id; - } - - private static string BuildMessage(SessionAggregate session) => new ErrorMessageBuilder(ErrorMessage) - .AddData(nameof(SessionId), session.Id.Value) - .Build(); -} diff --git a/old/src/Logitar.Identity.Domain/Sessions/SessionIsNotPersistentException.cs b/old/src/Logitar.Identity.Domain/Sessions/SessionIsNotPersistentException.cs deleted file mode 100644 index fd97a7a..0000000 --- a/old/src/Logitar.Identity.Domain/Sessions/SessionIsNotPersistentException.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Logitar.Identity.Domain.Shared; - -namespace Logitar.Identity.Domain.Sessions; - -/// -/// The exception raised when an ephemeral (not persistent) session is renewed. -/// -public class SessionIsNotPersistentException : InvalidCredentialsException -{ - /// - /// A generic error message for this exception. - /// - public new const string ErrorMessage = "The specified session is not persistent."; - - /// - /// Gets the identifier of the ephemeral session. - /// - public SessionId SessionId - { - get => new((string)Data[nameof(SessionId)]!); - private set => Data[nameof(SessionId)] = value.Value; - } - - /// - /// Initializes a new instance of the class. - /// - /// The session that is ephemeral. - public SessionIsNotPersistentException(SessionAggregate session) : base(BuildMessage(session)) - { - SessionId = session.Id; - } - - private static string BuildMessage(SessionAggregate session) => new ErrorMessageBuilder(ErrorMessage) - .AddData(nameof(SessionId), session.Id.Value) - .Build(); -} diff --git a/old/src/Logitar.Identity.Domain/Users/UserAggregate.cs b/old/src/Logitar.Identity.Domain/Users/UserAggregate.cs deleted file mode 100644 index 4daa5d8..0000000 --- a/old/src/Logitar.Identity.Domain/Users/UserAggregate.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Logitar.EventSourcing; -using Logitar.Identity.Domain.Sessions; - -namespace Logitar.Identity.Domain.Users; - -public class UserAggregate : AggregateRoot -{ - /// - /// Signs-in the user without a password check, opening a new session. - /// - /// The secret of the session. - /// (Optional) The actor identifier. This parameter should be left null so that it defaults to the user's identifier. - /// The identifier of the session. - /// The newly opened session. - /// The user is disabled. - public SessionAggregate SignIn(Password? secret = null, ActorId? actorId = null, SessionId? sessionId = null) - { - return SignIn(password: null, secret, actorId, sessionId); - } - /// - /// Signs-in the user, opening a new session. - /// - /// The password to check. - /// The secret of the session. - /// (Optional) The actor identifier. This parameter should be left null so that it defaults to the user's identifier. - /// The identifier of the session. - /// The newly opened session. - /// The password is incorrect. - /// The user has no password. - /// The user is disabled. - public SessionAggregate SignIn(string? password, Password? secret = null, ActorId? actorId = null, SessionId? sessionId = null) - { - if (IsDisabled) - { - throw new UserIsDisabledException(this); - } - else if (password != null) - { - if (_password == null) - { - throw new UserHasNoPasswordException(this); - } - else if (!_password.IsMatch(password)) - { - throw new IncorrectUserPasswordException(this, password); - } - } - - actorId ??= new(Id.Value); - SessionAggregate session = new(this, secret, actorId, sessionId); - Raise(new UserSignedInEvent(session.CreatedOn), actorId.Value); - - return session; - } - /// - /// Applies the specified event. - /// - /// The event to apply. - protected virtual void Apply(UserSignedInEvent @event) - { - AuthenticatedOn = @event.OccurredOn; - } -}