From b7c21e69894b34944d5b7d51c9547a192d54512e Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:09:57 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`Dev`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @dariemcarlosdev. * https://github.com/dariemcarlosdev/SecureCleanApiWaf/pull/1#issuecomment-3563685039 The following files were modified: * `Core/Application/Common/Behaviors/CachingBehavior.cs` * `Core/Application/Common/DTOs/TokenBlacklistStatusDto.cs` * `Core/Application/Common/Interfaces/IApiDataItemRepository.cs` * `Core/Application/Common/Interfaces/IApiIntegrationService.cs` * `Core/Application/Common/Interfaces/ICacheService.cs` * `Core/Application/Common/Interfaces/ITokenBlacklistService.cs` * `Core/Application/Common/Interfaces/ITokenRepository.cs` * `Core/Application/Common/Interfaces/IUserRepository.cs` * `Core/Application/Common/Mapping/ApiDataMapper.cs` * `Core/Application/Common/Models/Result.cs` * `Core/Application/Common/Profiles/ApiDataMappingProfile.cs` * `Core/Application/Features/Authentication/Commands/BlacklistTokenCommand.cs` * `Core/Application/Features/Authentication/Commands/BlacklistTokenCommandHandler.cs` * `Core/Application/Features/Authentication/Commands/LoginUserCommand.cs` * `Core/Application/Features/Authentication/Commands/LoginUserCommandHandler.cs` * `Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQuery.cs` * `Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQueryHandler.cs` * `Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQuery.cs` * `Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQueryHandler.cs` * `Core/Application/Features/SampleData/Queries/GetApiDataByIdQuery.cs` * `Core/Application/Features/SampleData/Queries/GetApiDataByIdQueryHandler.cs` * `Core/Application/Features/SampleData/Queries/GetApiDataQuery.cs` * `Core/Application/Features/SampleData/Queries/GetApiDataQueryHandler.cs` * `Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQuery.cs` * `Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQueryHandler.cs` * `Core/Domain/Entities/ApiDataItem.cs` * `Core/Domain/Entities/BaseEntity.cs` * `Core/Domain/Entities/Token.cs` * `Core/Domain/Entities/User.cs` * `Core/Domain/Events/BaseDomainEvent.cs` * `Core/Domain/Events/TokenRevokedEvent.cs` * `Core/Domain/Events/UserRegisteredEvent.cs` * `Core/Domain/Exceptions/DomainException.cs` * `Core/Domain/ValueObjects/Email.cs` * `Core/Domain/ValueObjects/Role.cs` * `Core/Domain/ValueObjects/ValueObject.cs` * `Infrastructure/Caching/CacheService.cs` * `Infrastructure/Caching/SampleCache.cs` * `Infrastructure/Data/ApplicationDbContext.cs` * `Infrastructure/Data/Configurations/ApiDataItemConfiguration.cs` * `Infrastructure/Data/DatabaseSettings.cs` * `Infrastructure/Handlers/ApiKeyHandler.cs` * `Infrastructure/Middleware/JwtBlacklistValidationMiddleware.cs` * `Infrastructure/Repositories/ApiDataItemRepository.cs` * `Infrastructure/Repositories/TokenRepository.cs` * `Infrastructure/Repositories/UserRepository.cs` * `Infrastructure/Security/JwtTokenGenerator.cs` * `Infrastructure/Services/ApiIntegrationService.cs` * `Infrastructure/Services/TokenBlacklistService.cs` * `Presentation/Controllers/v1/AuthController.cs` * `Presentation/Controllers/v1/SampleController.cs` * `Presentation/Controllers/v1/TokenBlacklistController.cs` * `Presentation/Extensions/DependencyInjection/ApplicationServiceExtensions.cs` * `Presentation/Extensions/DependencyInjection/InfrastructureServiceExtensions.cs` * `Presentation/Extensions/DependencyInjection/PresentationServiceExtensions.cs` * `Presentation/Extensions/HttpPipeline/WebApplicationExtensions.cs` --- .../Common/Behaviors/CachingBehavior.cs | 11 +- .../Common/DTOs/TokenBlacklistStatusDto.cs | 19 ++- .../Interfaces/IApiDataItemRepository.cs | 115 +++++++++++++--- .../Interfaces/IApiIntegrationService.cs | 46 ++++++- .../Common/Interfaces/ICacheService.cs | 35 ++++- .../Interfaces/ITokenBlacklistService.cs | 31 ++++- .../Common/Interfaces/ITokenRepository.cs | 101 +++++++++++--- .../Common/Interfaces/IUserRepository.cs | 89 ++++++++++-- .../Common/Mapping/ApiDataMapper.cs | 44 +++++- Core/Application/Common/Models/Result.cs | 14 +- .../Common/Profiles/ApiDataMappingProfile.cs | 11 +- .../Commands/BlacklistTokenCommand.cs | 11 +- .../Commands/BlacklistTokenCommandHandler.cs | 24 +++- .../Commands/LoginUserCommand.cs | 12 +- .../Commands/LoginUserCommandHandler.cs | 26 +++- .../Queries/GetTokenBlacklistStatsQuery.cs | 7 +- .../GetTokenBlacklistStatsQueryHandler.cs | 46 +++++-- .../Queries/IsTokenBlacklistedQuery.cs | 15 +- .../Queries/IsTokenBlacklistedQueryHandler.cs | 18 ++- .../SampleData/Queries/GetApiDataByIdQuery.cs | 9 +- .../Queries/GetApiDataByIdQueryHandler.cs | 9 +- .../SampleData/Queries/GetApiDataQuery.cs | 8 +- .../Queries/GetApiDataQueryHandler.cs | 22 ++- .../Queries/GetApiDataWithMappingQuery.cs | 8 +- .../GetApiDataWithMappingQueryHandler.cs | 27 +++- Core/Domain/Entities/ApiDataItem.cs | 110 +++++++++++++-- Core/Domain/Entities/BaseEntity.cs | 22 ++- Core/Domain/Entities/Token.cs | 94 +++++++++++-- Core/Domain/Entities/User.cs | 129 +++++++++++++++--- Core/Domain/Events/BaseDomainEvent.cs | 8 +- Core/Domain/Events/TokenRevokedEvent.cs | 14 +- Core/Domain/Events/UserRegisteredEvent.cs | 15 +- Core/Domain/Exceptions/DomainException.cs | 30 +++- Core/Domain/ValueObjects/Email.cs | 54 ++++++-- Core/Domain/ValueObjects/Role.cs | 55 ++++++-- Core/Domain/ValueObjects/ValueObject.cs | 26 +++- Infrastructure/Caching/CacheService.cs | 25 +++- Infrastructure/Caching/SampleCache.cs | 13 +- Infrastructure/Data/ApplicationDbContext.cs | 27 +++- .../ApiDataItemConfiguration.cs | 7 +- Infrastructure/Data/DatabaseSettings.cs | 12 +- Infrastructure/Handlers/ApiKeyHandler.cs | 11 +- .../JwtBlacklistValidationMiddleware.cs | 35 ++++- .../Repositories/ApiDataItemRepository.cs | 76 +++++++++-- .../Repositories/TokenRepository.cs | 45 ++++-- Infrastructure/Repositories/UserRepository.cs | 35 ++++- Infrastructure/Security/JwtTokenGenerator.cs | 24 +++- .../Services/ApiIntegrationService.cs | 48 ++++++- .../Services/TokenBlacklistService.cs | 42 +++++- Presentation/Controllers/v1/AuthController.cs | 45 +++++- .../Controllers/v1/SampleController.cs | 37 ++++- .../v1/TokenBlacklistController.cs | 34 ++++- .../ApplicationServiceExtensions.cs | 22 ++- .../InfrastructureServiceExtensions.cs | 22 ++- .../PresentationServiceExtensions.cs | 6 +- .../HttpPipeline/WebApplicationExtensions.cs | 6 +- 56 files changed, 1605 insertions(+), 282 deletions(-) diff --git a/Core/Application/Common/Behaviors/CachingBehavior.cs b/Core/Application/Common/Behaviors/CachingBehavior.cs index 9d08e84..489d8d4 100644 --- a/Core/Application/Common/Behaviors/CachingBehavior.cs +++ b/Core/Application/Common/Behaviors/CachingBehavior.cs @@ -30,7 +30,7 @@ namespace SecureCleanApiWaf.Core.Application.Common.Behaviors /// /// In a CQRS (Command Query Responsibility Segregation) architecture, MediatR is often used to dispatch commands (for state changes) /// and queries (for data retrieval) to their respective handlers. This caching behavior is especially useful for queries, as it can - /// intercept query requests, check if the result is already cached, and return the cached data if available—reducing load on the system + /// intercept query requests, check if the result is already cached, and return the cached data if available�reducing load on the system /// and improving performance. For commands, which change state, caching is typically bypassed. This ensures that queries are fast and /// scalable, while commands remain consistent and reliable, fully supporting the separation of read and write concerns central to CQRS. /// @@ -41,6 +41,13 @@ IDistributedCache cache : IPipelineBehavior where TRequest : ICacheable { + /// + /// Intercepts cacheable requests to return a cached response when available or invoke the handler, cache its result, and return that response. + /// + /// The cacheable request that provides the cache key and controls behavior (set BypassCache to skip caching; may specify SlidingExpirationInMinutes and AbsoluteExpirationInMinutes). + /// The handler delegate to execute when a cached response is not available or caching is bypassed. + /// Cancellation token used for cache operations and handler execution. + /// The cached response if present for the request's CacheKey; otherwise the response produced by invoking the handler. public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { TResponse response; @@ -98,4 +105,4 @@ async Task GetResponseAndAddToCache() return response; } } -} +} \ No newline at end of file diff --git a/Core/Application/Common/DTOs/TokenBlacklistStatusDto.cs b/Core/Application/Common/DTOs/TokenBlacklistStatusDto.cs index ca17551..239d186 100644 --- a/Core/Application/Common/DTOs/TokenBlacklistStatusDto.cs +++ b/Core/Application/Common/DTOs/TokenBlacklistStatusDto.cs @@ -48,7 +48,14 @@ public class TokenBlacklistStatusDto /// /// Creates a blacklisted token status. + /// + /// Create a TokenBlacklistStatusDto representing a token that has been blacklisted. /// + /// The token's JWT ID (jti), or null if unknown. + /// The UTC time when the token was blacklisted, or null if not recorded. + /// The token's natural expiration time, or null if unknown. + /// Whether the status was retrieved from cache. + /// A TokenBlacklistStatusDto with IsBlacklisted set to `true`, Status set to "blacklisted", Details describing the blacklist, CheckedAt set to the current UTC time, and FromCache set to the provided value. public static TokenBlacklistStatusDto Blacklisted( string? tokenId, DateTime? blacklistedAt, @@ -70,7 +77,13 @@ public static TokenBlacklistStatusDto Blacklisted( /// /// Creates a valid (not blacklisted) token status. + /// + /// Creates a TokenBlacklistStatusDto representing a valid (not blacklisted) token. /// + /// The token's JWT ID (jti), or null if unavailable. + /// The token's natural expiration time, or null if unknown. + /// Whether the result was retrieved from cache. + /// A DTO indicating the token is valid; CheckedAt is set to the current UTC time. public static TokenBlacklistStatusDto Valid( string? tokenId, DateTime? tokenExpiresAt, @@ -90,7 +103,11 @@ public static TokenBlacklistStatusDto Valid( /// /// Creates an invalid token status (malformed or expired). + /// + /// Create a TokenBlacklistStatusDto representing an invalid token status. /// + /// Human-readable explanation for why the token is considered invalid. + /// A TokenBlacklistStatusDto with IsBlacklisted set to false, Status set to "invalid", Details set to the provided reason, CheckedAt set to the current UTC time, and FromCache set to false. public static TokenBlacklistStatusDto Invalid(string reason) { return new TokenBlacklistStatusDto @@ -103,4 +120,4 @@ public static TokenBlacklistStatusDto Invalid(string reason) }; } } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Interfaces/IApiDataItemRepository.cs b/Core/Application/Common/Interfaces/IApiDataItemRepository.cs index c025c0c..fd8d5f0 100644 --- a/Core/Application/Common/Interfaces/IApiDataItemRepository.cs +++ b/Core/Application/Common/Interfaces/IApiDataItemRepository.cs @@ -63,7 +63,11 @@ public interface IApiDataItemRepository /// /// The item's unique identifier. /// Cancellation token. - /// The item if found, null otherwise. + /// +/// Retrieves an ApiDataItem by its unique identifier. +/// +/// The unique identifier of the ApiDataItem to retrieve. +/// The ApiDataItem with the specified id, or `null` if no matching item is found. Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); /// @@ -76,7 +80,11 @@ public interface IApiDataItemRepository /// Used to find existing items when synchronizing from external APIs. /// Prevents duplicate storage of the same external data. /// Should be optimized with database index on ExternalId. - /// + /// +/// Retrieves an that matches the specified external system identifier. +/// +/// The identifier assigned to the item by an external system. +/// The matching , or null if no match exists. Task GetByExternalIdAsync(string externalId, CancellationToken cancellationToken = default); /// @@ -87,7 +95,10 @@ public interface IApiDataItemRepository /// /// Returns only items with Active status. /// Used for serving fresh data to API consumers. - /// + /// +/// Retrieves all active API data items — items that are not marked as deleted and are not marked as stale. +/// +/// A read-only list of active instances. Task> GetActiveItemsAsync(CancellationToken cancellationToken = default); /// @@ -99,7 +110,10 @@ public interface IApiDataItemRepository /// Includes active, stale, and deleted items. /// Used for administrative purposes and full data exports. /// Consider pagination for large datasets. - /// + /// +/// Retrieves all API data items regardless of their status (including deleted or stale items). +/// +/// A read-only list containing every in the repository. Task> GetAllItemsAsync(CancellationToken cancellationToken = default); /// @@ -111,7 +125,12 @@ public interface IApiDataItemRepository /// /// Useful for batch operations like refreshing stale items /// or cleaning up deleted items. - /// + /// +/// Retrieves all ApiDataItem instances that have the specified data status. +/// +/// The DataStatus value to filter items by. +/// Token to observe while waiting for the task to complete. +/// A read-only list of ApiDataItem objects whose status equals the specified value. Task> GetItemsByStatusAsync(DataStatus status, CancellationToken cancellationToken = default); /// @@ -124,7 +143,11 @@ public interface IApiDataItemRepository /// Returns items where (UtcNow - LastSyncedAt) > maxAge. /// Used by background refresh jobs to identify stale data. /// Should be optimized with index on LastSyncedAt. - /// + /// +/// Finds API data items whose last synchronization time is older than the provided maximum age and therefore need refreshing. +/// +/// The maximum allowed age since an item's LastSyncedAt; items older than this value are considered to need refresh. +/// A read-only list of ApiDataItem instances whose LastSyncedAt age exceeds . Task> GetItemsNeedingRefreshAsync(TimeSpan maxAge, CancellationToken cancellationToken = default); /// @@ -136,7 +159,11 @@ public interface IApiDataItemRepository /// /// Useful for managing data from multiple API sources. /// Enables source-specific refresh or cleanup operations. - /// + /// +/// Retrieves API data items that originate from the specified source URL. +/// +/// The source URL to filter items by. +/// A read-only list of instances that originate from the specified source URL. Task> GetItemsBySourceUrlAsync(string sourceUrl, CancellationToken cancellationToken = default); /// @@ -149,7 +176,11 @@ public interface IApiDataItemRepository /// Performs case-insensitive partial match on Name field. /// Useful for search functionality in API endpoints. /// Consider adding pagination for large result sets. - /// + /// +/// Finds ApiDataItem objects whose names match a case-insensitive partial search term. +/// +/// The substring to match against item names; matching is case-insensitive and treats the term as a partial search. +/// A read-only list of ApiDataItem instances whose names match the provided search term. Task> SearchByNameAsync(string searchTerm, CancellationToken cancellationToken = default); /// @@ -162,7 +193,12 @@ public interface IApiDataItemRepository /// /// Searches JSON metadata field for specific keys/values. /// Implementation depends on database JSON support. - /// + /// +/// Retrieves ApiDataItem entities that contain a specified metadata key, optionally filtered to a specific metadata value. +/// +/// The metadata key to match. +/// An optional metadata value to match; when null, items containing the key are returned regardless of the value. +/// A read-only list of ApiDataItem objects matching the metadata criteria. Task> GetItemsByMetadataAsync(string metadataKey, object? metadataValue = null, CancellationToken cancellationToken = default); /// @@ -174,7 +210,11 @@ public interface IApiDataItemRepository /// /// Used to prevent duplicate storage of external data. /// Should check against non-deleted items only. - /// + /// +/// Checks whether a non-deleted ApiDataItem with the specified external system identifier exists. +/// +/// The external system identifier to check for. +/// `true` if a non-deleted item with the given external ID exists, `false` otherwise. Task ExistsAsync(string externalId, CancellationToken cancellationToken = default); /// @@ -186,7 +226,10 @@ public interface IApiDataItemRepository /// /// Creates a new record in the database. /// Typically called after fetching data from external API. - /// + /// +/// Adds a new ApiDataItem to the repository. +/// +/// The ApiDataItem to add; must not be null. The item will be tracked and persisted when SaveChangesAsync is called. Task AddAsync(ApiDataItem item, CancellationToken cancellationToken = default); /// @@ -198,7 +241,11 @@ public interface IApiDataItemRepository /// /// Optimized batch operation for bulk imports. /// More efficient than adding one by one. - /// + /// +/// Adds multiple ApiDataItem instances to the repository. +/// +/// The collection of ApiDataItem objects to add. +/// A token to cancel the operation. Task AddRangeAsync(IEnumerable items, CancellationToken cancellationToken = default); /// @@ -210,7 +257,11 @@ public interface IApiDataItemRepository /// /// Updates item data, status, metadata, etc. /// Called after refreshing data from external API. - /// + /// +/// Applies the provided ApiDataItem's updated values to the repository state. +/// +/// The ApiDataItem containing the updated data to store. +/// Token to cancel the operation. Task UpdateAsync(ApiDataItem item, CancellationToken cancellationToken = default); /// @@ -222,7 +273,12 @@ public interface IApiDataItemRepository /// /// Optimized batch operation for bulk updates. /// Useful for background refresh jobs. - /// + /// +/// Updates multiple ApiDataItem entities in a single batch operation. +/// +/// The collection of items to update. +/// Token to observe for cancellation. +/// The number of items updated. Task UpdateRangeAsync(IEnumerable items, CancellationToken cancellationToken = default); /// @@ -234,7 +290,10 @@ public interface IApiDataItemRepository /// /// Soft delete - marks item as deleted but preserves data. /// Use item.MarkAsDeleted() before calling this. - /// + /// +/// Marks the given ApiDataItem as deleted in the repository (soft delete) without removing its data. +/// +/// The ApiDataItem to soft-delete; the item is expected to be marked as deleted prior to calling this method. Task DeleteAsync(ApiDataItem item, CancellationToken cancellationToken = default); /// @@ -247,7 +306,12 @@ public interface IApiDataItemRepository /// Hard delete for cleanup of old deleted items. /// Should be used carefully, typically by scheduled cleanup jobs. /// Permanently removes data - cannot be recovered. - /// + /// +/// Permanently removes ApiDataItem records that were soft-deleted prior to the specified cutoff date. +/// +/// Remove items soft-deleted before this date and time (UTC is recommended). +/// Token to cancel the operation. +/// The number of items that were permanently removed. Task PermanentlyDeleteOldItemsAsync(DateTime olderThan, CancellationToken cancellationToken = default); /// @@ -259,7 +323,12 @@ public interface IApiDataItemRepository /// /// Bulk operation to invalidate cache for an entire API source. /// Useful when external API structure changes. - /// + /// +/// Marks all ApiDataItem entities that originate from the specified source URL as stale. +/// +/// The source URL whose items should be marked stale. +/// A token to cancel the operation. +/// The number of items that were marked as stale. Task MarkSourceAsStaleAsync(string sourceUrl, CancellationToken cancellationToken = default); /// @@ -270,7 +339,10 @@ public interface IApiDataItemRepository /// /// Provides insights for cache performance and data freshness monitoring. /// Returns counts by status, average age, etc. - /// + /// +/// Retrieves aggregated statistics for ApiDataItem entities such as counts by status, age metrics, and other summary telemetry. +/// +/// An containing counts, averages, and other summary metrics for ApiDataItem entities. Task GetStatisticsAsync(CancellationToken cancellationToken = default); /// @@ -281,7 +353,10 @@ public interface IApiDataItemRepository /// /// Commits the unit of work transaction. /// Should be called after Add/Update/Delete operations. - /// + /// +/// Persists all pending changes tracked by the repository to the underlying data store. +/// +/// The number of state entries written to the underlying data store. Task SaveChangesAsync(CancellationToken cancellationToken = default); } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Interfaces/IApiIntegrationService.cs b/Core/Application/Common/Interfaces/IApiIntegrationService.cs index 7dc3ef1..c522874 100644 --- a/Core/Application/Common/Interfaces/IApiIntegrationService.cs +++ b/Core/Application/Common/Interfaces/IApiIntegrationService.cs @@ -31,7 +31,12 @@ public interface IApiIntegrationService /// Example: "api/products" or "api/users" /// /// Generic method for flexibility - use when working with DTOs or dynamic responses. - /// + /// +/// Retrieve and deserialize all items from the specified API endpoint. +/// +/// The target type to deserialize the API response into. +/// Relative API endpoint path to request (appended to the configured base address). +/// A containing the deserialized data of type on success, or error information on failure. Task> GetAllDataAsync(string apiUrl); /// @@ -46,7 +51,12 @@ public interface IApiIntegrationService /// Example: apiUrl="api/products", id="123" ? GET /api/products/123 /// /// Generic method for flexibility - use when working with DTOs or dynamic responses. - /// + /// +/// Retrieve a single resource from the specified API endpoint by its identifier and deserialize it to type . +/// +/// Relative API endpoint path appended to the configured base address (e.g., "api/products"). +/// The resource identifier appended to to form the request path (e.g., "{id}"). +/// A containing the deserialized item on success, or error information on failure. Task> GetDataByIdAsync(string apiUrl, string id); /// @@ -74,7 +84,11 @@ public interface IApiIntegrationService /// } /// } /// ``` - /// + /// +/// Fetches data from the specified API endpoint and maps the response to a list of domain ApiDataItem entities. +/// +/// Relative API endpoint path (resolved against the configured base address), e.g. "products". +/// A Result containing the mapped list of on success, or error information on failure. Task>> GetApiDataItemsAsync(string apiUrl); /// @@ -99,7 +113,12 @@ public interface IApiIntegrationService /// } /// } /// ``` - /// + /// +/// Retrieve a single API data item by ID and map it to an ApiDataItem domain entity. +/// +/// Relative API endpoint path to which the will be appended (for example, "api/products"). +/// Identifier of the resource to fetch. +/// A Result<ApiDataItem> containing the mapped domain entity on success, or error information on failure. Task> GetApiDataItemByIdAsync(string apiUrl, string id); /// @@ -124,7 +143,11 @@ public interface IApiIntegrationService /// _logger.LogWarning("External API unavailable, using cached data"); /// } /// ``` - /// + /// +/// Checks whether the specified API endpoint is available and responsive. +/// +/// Relative path of the API endpoint to probe (based on the configured base address). +/// A Result containing `true` if the API responded and is healthy, `false` if it responded but is unhealthy or unresponsive, or error information on failure. Task> CheckApiHealthAsync(string apiUrl); /// @@ -146,7 +169,16 @@ public interface IApiIntegrationService /// page: 1, /// pageSize: 50); /// ``` - /// + /// +/// Retrieve a single page of items from the specified API endpoint and map the response into a paginated DTO. +/// +/// +/// The should be a relative endpoint appended to the configured API base address; the implementation will add paging query parameters for and when calling the upstream API. +/// +/// Relative API path (for example, "products"). +/// Page number to retrieve (starting at 1). +/// Number of items per page. +/// A Result containing a PaginatedResponseDto of items of type on success, or error information on failure. Task>> GetPaginatedDataAsync(string apiUrl, int page, int pageSize); } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Interfaces/ICacheService.cs b/Core/Application/Common/Interfaces/ICacheService.cs index e4dac66..62f02e6 100644 --- a/Core/Application/Common/Interfaces/ICacheService.cs +++ b/Core/Application/Common/Interfaces/ICacheService.cs @@ -20,7 +20,12 @@ public interface ICacheService /// The cached value if found; otherwise, default(T). /// /// Returns null for reference types and default value for value types if the key doesn't exist. - /// + /// +/// Retrieves a cached value for the specified key. +/// +/// The cache key to look up. +/// A token to cancel the operation. +/// The cached value if present; `null` for reference types or `default(T)` for value types when the key is not found. Task GetAsync(string key, CancellationToken cancellationToken = default); /// @@ -35,7 +40,16 @@ public interface ICacheService /// /// If an item with the same key already exists, it will be overwritten. /// The expiration time is absolute from the time of insertion. - /// + /// +/// Stores a value in the distributed cache under the specified key. +/// +/// Cache key to store the value under. +/// Value to store in the cache. +/// Optional absolute expiration relative to now; if null a default of 5 minutes is applied. +/// Token to cancel the operation. +/// +/// If an entry with the same key already exists it will be overwritten. The provided expiration is absolute from the time of insertion. +/// Task SetAsync(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default); /// @@ -46,7 +60,14 @@ public interface ICacheService /// A task representing the asynchronous operation. /// /// If the key doesn't exist, the operation completes successfully without error. - /// + /// +/// Removes the cached entry identified by the provided key. +/// +/// The cache key to remove. +/// A token to cancel the operation. +/// +/// Completes successfully if the key does not exist; no error is thrown for missing keys. +/// Task RemoveAsync(string key, CancellationToken cancellationToken = default); /// @@ -54,7 +75,11 @@ public interface ICacheService /// /// The unique identifier for the cached item. /// A cancellation token to cancel the operation. - /// True if the key exists; otherwise, false. + /// +/// Determines whether a value exists in the cache for the specified key. +/// +/// The cache key to check for existence. +/// `true` if the key exists in the cache, `false` otherwise. Task ExistsAsync(string key, CancellationToken cancellationToken = default); } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Interfaces/ITokenBlacklistService.cs b/Core/Application/Common/Interfaces/ITokenBlacklistService.cs index 645dee8..ce01a33 100644 --- a/Core/Application/Common/Interfaces/ITokenBlacklistService.cs +++ b/Core/Application/Common/Interfaces/ITokenBlacklistService.cs @@ -46,7 +46,14 @@ public interface ITokenBlacklistService /// Error Handling: /// - Invalid tokens are logged but don't throw exceptions /// - Cache failures are logged and handled gracefully - /// + /// +/// Add the given JWT to the blacklist to prevent its future use. +/// +/// The JWT to blacklist; the token's JTI (unique identifier) and expiration are used to control the blacklist entry lifetime. +/// Cancellation token to cancel the operation. +/// +/// The service extracts the token identifier (JTI) and stores only that identifier with an expiration aligned to the token's expiry. Malformed or invalid tokens are handled gracefully and do not cause exceptions to be thrown; cache or storage errors are logged and handled without propagating sensitive details. +/// Task BlacklistTokenAsync(string jwtToken, CancellationToken cancellationToken = default); /// @@ -68,7 +75,12 @@ public interface ITokenBlacklistService /// - Handles malformed tokens gracefully /// - Logs suspicious token validation attempts /// - Never throws exceptions that could leak information - /// + /// +/// Determines whether the provided JWT is currently present in the token blacklist. +/// +/// The JWT string to check (expected to contain a JTI). +/// A token to observe while waiting for the operation to complete. +/// `true` if the token is blacklisted, `false` otherwise. Task IsTokenBlacklistedAsync(string jwtToken, CancellationToken cancellationToken = default); /// @@ -92,7 +104,13 @@ public interface ITokenBlacklistService /// /// Note: In distributed cache scenarios (Redis), expired entries /// are often cleaned up automatically by the cache provider. - /// + /// +/// Removes expired entries from the token blacklist to reclaim resources. +/// +/// +/// Intended to be executed periodically (e.g., at startup, shutdown, or by a background task) to purge tokens that have passed their expiration. In distributed-cache scenarios, some expirations may be handled by the cache provider and therefore not require manual cleanup. +/// +/// The number of blacklist entries removed during this cleanup operation. Task CleanupExpiredTokensAsync(CancellationToken cancellationToken = default); /// @@ -112,7 +130,10 @@ public interface ITokenBlacklistService /// - Administrative dashboards /// - Performance monitoring /// - Capacity planning - /// + /// +/// Retrieve monitoring and debugging statistics for the token blacklist. +/// +/// A containing total blacklisted tokens, expired tokens pending cleanup, estimated memory usage in bytes, an optional cache hit rate percentage, and the UTC timestamp when the statistics were last calculated. Task GetBlacklistStatsAsync(CancellationToken cancellationToken = default); } @@ -150,4 +171,4 @@ public class TokenBlacklistStats /// public DateTime LastUpdated { get; set; } = DateTime.UtcNow; } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Interfaces/ITokenRepository.cs b/Core/Application/Common/Interfaces/ITokenRepository.cs index f8fb9e5..2e58a27 100644 --- a/Core/Application/Common/Interfaces/ITokenRepository.cs +++ b/Core/Application/Common/Interfaces/ITokenRepository.cs @@ -60,7 +60,11 @@ public interface ITokenRepository /// /// The token's unique identifier. /// Cancellation token. - /// The token if found, null otherwise. + /// +/// Retrieves a token by its unique identifier. +/// +/// The token's unique identifier. +/// The token with the specified identifier, or null if not found. Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); /// @@ -72,7 +76,15 @@ public interface ITokenRepository /// /// Primary method for token validation during authentication. /// Should be optimized with database index on TokenId. - /// + /// +/// Retrieves a token by its JTI (token identifier). +/// +/// The token's JTI (JWT ID) to look up. +/// Cancellation token to cancel the operation. +/// The matching if found, or `null` if no token exists for the specified `tokenId`. +/// +/// This lookup is intended for fast validation paths and is typically backed by an index on the token identifier. +/// Task GetByTokenIdAsync(string tokenId, CancellationToken cancellationToken = default); /// @@ -84,7 +96,11 @@ public interface ITokenRepository /// /// Used for user session management and security auditing. /// Returns all tokens (active, revoked, expired). - /// + /// +/// Retrieves all tokens associated with the specified user, including active, revoked, and expired tokens. +/// +/// The unique identifier of the user whose tokens are being retrieved. +/// An IReadOnlyList of Token entities belonging to the user; the list may include active, revoked, and expired tokens. Task> GetTokensByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); /// @@ -96,7 +112,11 @@ public interface ITokenRepository /// /// Used to display current user sessions or revoke all sessions. /// Only returns tokens with Active status and not expired. - /// + /// +/// Gets active (not revoked and not expired) tokens for the specified user. +/// +/// The unique identifier of the user whose active tokens to retrieve. +/// A read-only list of tokens that are active for the specified user; the list is empty if none are found. Task> GetActiveTokensByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); /// @@ -105,7 +125,12 @@ public interface ITokenRepository /// The user's unique identifier. /// The token type (Access or Refresh). /// Cancellation token. - /// List of tokens of the specified type. + /// +/// Retrieves tokens belonging to the specified user filtered by the given token type. +/// +/// The unique identifier of the user whose tokens to retrieve. +/// The token type to filter by (e.g., Access or Refresh). +/// An IReadOnlyList of tokens of the specified type for the user; empty if none are found. Task> GetTokensByUserAndTypeAsync(Guid userId, TokenType tokenType, CancellationToken cancellationToken = default); /// @@ -116,7 +141,10 @@ public interface ITokenRepository /// /// Used for security auditing and blacklist management. /// Consider pagination for production use. - /// + /// +/// Retrieves all tokens that have been revoked for auditing and blacklist management. +/// +/// A read-only list of revoked Token entities; empty if none are found. Task> GetRevokedTokensAsync(CancellationToken cancellationToken = default); /// @@ -127,7 +155,10 @@ public interface ITokenRepository /// /// Used by background cleanup jobs to remove old token records. /// Consider pagination for production use. - /// + /// +/// Retrieves tokens that have passed their expiration time and are candidates for cleanup. +/// +/// An IReadOnlyList of tokens that are expired and eligible for removal. Task> GetExpiredTokensAsync(CancellationToken cancellationToken = default); /// @@ -139,7 +170,11 @@ public interface ITokenRepository /// /// Optimized query for fast token validation during authentication. /// Should use indexed queries for performance. - /// + /// +/// Determines whether a token with the specified token ID (JTI) exists and is currently valid. +/// +/// The token's unique identifier (JTI). +/// `true` if the token exists and is not revoked or expired, `false` otherwise. Task IsTokenValidAsync(string tokenId, CancellationToken cancellationToken = default); /// @@ -151,7 +186,11 @@ public interface ITokenRepository /// /// Used during authentication to reject blacklisted tokens. /// Should be fast query with database index. - /// + /// +/// Determines whether a token identified by its JTI is blacklisted. +/// +/// The token's unique identifier (JTI). +/// `true` if the token is blacklisted (revoked), `false` otherwise. Task IsTokenBlacklistedAsync(string tokenId, CancellationToken cancellationToken = default); /// @@ -163,7 +202,13 @@ public interface ITokenRepository /// /// Creates a new token record in the database. /// Typically called after JWT generation. - /// + /// +/// Adds a new Token entity to the repository. +/// +/// The Token entity to add (typically created after JWT issuance). +/// +/// The addition is staged in the repository; call SaveChangesAsync to persist changes to the underlying store. +/// Task AddAsync(Token token, CancellationToken cancellationToken = default); /// @@ -175,7 +220,11 @@ public interface ITokenRepository /// /// Updates token status, revocation info, etc. /// Should handle domain events if any were raised. - /// + /// +/// Updates an existing token entity in the repository. +/// +/// The token entity with changes to apply (must identify an existing token). +/// A token to observe while waiting for the task to complete. Task UpdateAsync(Token token, CancellationToken cancellationToken = default); /// @@ -187,7 +236,10 @@ public interface ITokenRepository /// /// Hard delete for expired tokens cleanup. /// For revocation, use Update with token.Revoke() instead. - /// + /// +/// Permanently removes the specified token from the repository. +/// +/// The token entity to delete. Task DeleteAsync(Token token, CancellationToken cancellationToken = default); /// @@ -199,7 +251,11 @@ public interface ITokenRepository /// /// Optimized batch operation for cleanup jobs. /// More efficient than deleting one by one. - /// + /// +/// Deletes the provided expired tokens in a single batch operation. +/// +/// Collection of expired Token entities to remove. +/// The number of tokens that were deleted. Task DeleteExpiredTokensAsync(IEnumerable tokens, CancellationToken cancellationToken = default); /// @@ -212,7 +268,12 @@ public interface ITokenRepository /// /// Used for "logout all sessions" functionality or security actions. /// Bulk operation that revokes all user's active tokens. - /// + /// +/// Revokes all active tokens belonging to the specified user. +/// +/// The unique identifier of the user whose tokens will be revoked. +/// A brief reason to record for the revocation (audit/metadata). +/// The number of tokens that were revoked. Task RevokeAllUserTokensAsync(Guid userId, string reason, CancellationToken cancellationToken = default); /// @@ -223,7 +284,10 @@ public interface ITokenRepository /// /// Provides insights for security monitoring and capacity planning. /// Returns counts of active, revoked, expired tokens. - /// + /// +/// Retrieves aggregated token usage and lifecycle metrics for monitoring and auditing. +/// +/// A instance containing counts of active, revoked, and expired tokens, counts by token type (access/refresh), deltas for the last 24 hours, and the timestamp when the metrics were calculated. Task GetTokenStatisticsAsync(CancellationToken cancellationToken = default); /// @@ -234,7 +298,10 @@ public interface ITokenRepository /// /// Commits the unit of work transaction. /// Should be called after Add/Update/Delete operations. - /// + /// +/// Persists pending repository changes to the underlying data store. +/// +/// The number of state entries written to the data store. Task SaveChangesAsync(CancellationToken cancellationToken = default); } @@ -283,4 +350,4 @@ public class TokenStatistics /// public DateTime CalculatedAt { get; set; } = DateTime.UtcNow; } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Interfaces/IUserRepository.cs b/Core/Application/Common/Interfaces/IUserRepository.cs index ed3e262..93224b4 100644 --- a/Core/Application/Common/Interfaces/IUserRepository.cs +++ b/Core/Application/Common/Interfaces/IUserRepository.cs @@ -54,7 +54,11 @@ public interface IUserRepository /// /// The user's unique identifier. /// Cancellation token. - /// The user if found, null otherwise. + /// +/// Retrieve a user by their unique identifier. +/// +/// The user's unique identifier. +/// The user with the specified ID, or null if no matching user is found. Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); /// @@ -66,7 +70,14 @@ public interface IUserRepository /// /// Used primarily for authentication/login operations. /// Should perform case-insensitive comparison. - /// + /// +/// Retrieves a user by username using a case-insensitive comparison. +/// +/// The username to look up (comparison should be case-insensitive). +/// The matching , or null if no user exists with the given username. +/// +/// Primarily used for authentication and login flows; implementations should consider only non-deleted users when performing the lookup. +/// Task GetByUsernameAsync(string username, CancellationToken cancellationToken = default); /// @@ -78,7 +89,15 @@ public interface IUserRepository /// /// Used for password reset, email verification, etc. /// Should perform case-insensitive comparison. - /// + /// +/// Retrieve a user by email address using a case-insensitive comparison. +/// +/// The email address to find. +/// A token to cancel the operation. +/// The matching if found, or null otherwise. +/// +/// Should consider only non-deleted users and be suitable for scenarios such as password reset and account verification. +/// Task GetByEmailAsync(Email email, CancellationToken cancellationToken = default); /// @@ -89,7 +108,11 @@ public interface IUserRepository /// List of users with the specified role. /// /// Useful for administrative operations and reporting. - /// + /// +/// Retrieve all users assigned the specified role. +/// +/// The role used to filter users. +/// A read-only list of users that have the specified role. Task> GetUsersByRoleAsync(Role role, CancellationToken cancellationToken = default); /// @@ -101,7 +124,14 @@ public interface IUserRepository /// /// Should validate that username and email are unique before adding. /// Raises domain events (UserRegisteredEvent) that should be published after persistence. - /// + /// +/// Adds a new User to the repository. +/// +/// The User aggregate to persist; username and email must be unique among non-deleted users. +/// Token to cancel the operation. +/// +/// Implementations should validate uniqueness of username and email (considering non-deleted users) and may publish domain events (for example, a UserRegisteredEvent) after the user has been persisted. +/// Task AddAsync(User user, CancellationToken cancellationToken = default); /// @@ -113,7 +143,13 @@ public interface IUserRepository /// /// Updates all modified properties including roles. /// Should handle domain events if any were raised during entity modifications. - /// + /// +/// Updates an existing user's state in the repository. +/// +/// The User aggregate with updated values to persist. +/// +/// Implementations should persist all modified properties (including roles) and handle any domain events raised by the aggregate. +/// Task UpdateAsync(User user, CancellationToken cancellationToken = default); /// @@ -125,7 +161,14 @@ public interface IUserRepository /// /// Performs soft delete by setting IsDeleted flag. /// User data is preserved for audit purposes. - /// + /// +/// Marks the specified user as deleted in the repository using a soft-delete strategy. +/// +/// The user entity to delete; its state will be updated to indicate deletion. +/// A token to monitor for cancellation requests. +/// +/// Implementations should persist the soft-delete (for example by setting an `IsDeleted` flag), preserve the record for auditing, and handle any domain events raised during deletion. +/// Task DeleteAsync(User user, CancellationToken cancellationToken = default); /// @@ -137,7 +180,15 @@ public interface IUserRepository /// /// Used during user registration to ensure uniqueness. /// Should check against non-deleted users only. - /// + /// +/// Checks whether a username is already registered in the repository. +/// +/// The username to check (comparison should be case-insensitive). +/// `true` if a non-deleted user with the specified username exists, `false` otherwise. +/// +/// Comparison should ignore case and consider only users that are not soft-deleted. +/// This is typically used during user registration to enforce username uniqueness. +/// Task UsernameExistsAsync(string username, CancellationToken cancellationToken = default); /// @@ -149,7 +200,11 @@ public interface IUserRepository /// /// Used during user registration to ensure uniqueness. /// Should check against non-deleted users only. - /// + /// +/// Determines whether an active user with the specified email address already exists. +/// +/// The email address to check (value object representing an email). +/// `true` if a non-deleted user exists with the specified email (comparison is case-insensitive), `false` otherwise. Task EmailExistsAsync(Email email, CancellationToken cancellationToken = default); /// @@ -160,7 +215,14 @@ public interface IUserRepository /// List of users with excessive failed login attempts. /// /// Used for security monitoring and account lockout management. - /// + /// +/// Retrieves users whose recorded failed login attempts are greater than or equal to the specified threshold. +/// +/// Minimum number of failed login attempts a user must have to be included. +/// A read-only list of users whose failed login attempts meet or exceed the threshold. +/// +/// Intended for security monitoring and lockout management; implementations should consider only active (non-deleted) users. +/// Task> GetUsersWithFailedLoginAttemptsAsync(int threshold, CancellationToken cancellationToken = default); /// @@ -171,7 +233,10 @@ public interface IUserRepository /// /// Commits the unit of work transaction. /// Should be called after Add/Update/Delete operations. - /// + /// +/// Persists all pending changes in the repository as a unit-of-work commit. +/// +/// The number of affected entities persisted to the data store. Task SaveChangesAsync(CancellationToken cancellationToken = default); } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Mapping/ApiDataMapper.cs b/Core/Application/Common/Mapping/ApiDataMapper.cs index 6667656..f1cb97d 100644 --- a/Core/Application/Common/Mapping/ApiDataMapper.cs +++ b/Core/Application/Common/Mapping/ApiDataMapper.cs @@ -46,6 +46,10 @@ public class ApiDataMapper { private readonly ILogger _logger; + /// + /// Initializes a new instance of using the provided logger. + /// + /// Thrown when is null. public ApiDataMapper(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -59,7 +63,12 @@ public ApiDataMapper(ILogger logger) /// ApiDataItem domain entity. /// /// Handles common API response structures. Customize based on your actual API format. - /// + /// + /// Maps a single dynamic API response item to an ApiDataItem domain entity. + /// + /// The dynamic API response object containing fields such as id, name, description, and other metadata. + /// The originating API URL to record as the data source. + /// An ApiDataItem populated from the API item, or `null` if required fields are missing or an error occurs during mapping. public ApiDataItem? MapToApiDataItem(dynamic apiItem, string sourceUrl) { try @@ -104,7 +113,12 @@ public ApiDataMapper(ILogger logger) /// The type of API response collection. /// The API response containing items. /// The source API URL. - /// List of ApiDataItem domain entities. + /// + /// Map a dynamic API response into a list of ApiDataItem domain entities. + /// + /// The API response to map; may be an IEnumerable, an object containing a collection under `data`, `items`, or `results`, or a single item object. + /// The source URL used to populate the ApiDataItem's external source information. + /// A list of ApiDataItem created from the response; the list will be empty if no mappable items are found. public List MapToApiDataItems(T apiResponse, string sourceUrl) { var results = new List(); @@ -162,7 +176,12 @@ public List MapToApiDataItems(T apiResponse, string sourceUrl) /// /// The existing domain entity to update. /// The fresh API response item. - /// True if updated successfully, false otherwise. + /// + /// Update an existing ApiDataItem with values extracted from a dynamic API response item. + /// + /// The domain entity to update; it will be modified in place. + /// The dynamic API response object to extract values from (e.g., fields like "name"/"title", "description", and metadata). + /// `true` if the existingItem was updated and its metadata refreshed; `false` if required data was missing or an error occurred. public bool UpdateFromApiResponse(ApiDataItem existingItem, dynamic apiItem) { try @@ -198,7 +217,15 @@ public bool UpdateFromApiResponse(ApiDataItem existingItem, dynamic apiItem) /// Extracts metadata from API item and adds to domain entity. /// /// The domain entity to add metadata to. - /// The API response item. + /// + /// Extracts common metadata fields from a dynamic API item and adds them to the provided ApiDataItem. + /// + /// The domain entity to receive metadata entries. + /// The dynamic API response item to extract metadata from. + /// + /// Attempts to extract and add the following metadata keys when present: "category", "price", "rating", "tags", "status", and "source_timestamp". + /// Extraction failures are logged and do not prevent the method from completing. + /// private void AddMetadataFromApiItem(ApiDataItem apiDataItem, dynamic apiItem) { try @@ -254,7 +281,12 @@ private void AddMetadataFromApiItem(ApiDataItem apiDataItem, dynamic apiItem) /// /// The dynamic object. /// Possible property names to try. - /// The property value if found, null otherwise. + /// + /// Retrieve the first matching property value from a dynamic object using multiple candidate names. + /// + /// The dynamic object or dictionary to read properties from. + /// Candidate property names to try in order (matching is case-insensitive). + /// The value of the first property found, or null if none is present or an error occurs. private static object? GetPropertyValue(dynamic obj, params string[] propertyNames) { if (obj == null) return null; @@ -296,4 +328,4 @@ private void AddMetadataFromApiItem(ApiDataItem apiDataItem, dynamic apiItem) return null; } } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Models/Result.cs b/Core/Application/Common/Models/Result.cs index 8299411..3948074 100644 --- a/Core/Application/Common/Models/Result.cs +++ b/Core/Application/Common/Models/Result.cs @@ -16,9 +16,17 @@ namespace SecureCleanApiWaf.Core.Application.Common.Models /// langword="null"/>. public record Result(bool Success, T Data, string Error) { - // Factory method that returns an instance of a Result, encapsulate the creation logic, used to create a successful result with data. + /// +/// Create a successful Result<T> that contains the provided data. +/// +/// The value to assign to the result's Data when the operation succeeds. +/// A Result<T> with Success = true, Data set to the provided value, and Error = null. public static Result Ok(T data) => new Result(true, data, null); - // Factory method for failed result. The Data is set to default value of T. Here, default means null for reference types and zero or equivalent for value types. + /// +/// Create a failed Result<T> containing the provided error message. +/// +/// Error message describing the failure. +/// A Result<T> with Success = false, Data = default(T), and Error set to the provided message. public static Result Fail(string error) => new Result(false, default, error); } -} +} \ No newline at end of file diff --git a/Core/Application/Common/Profiles/ApiDataMappingProfile.cs b/Core/Application/Common/Profiles/ApiDataMappingProfile.cs index ff0539c..b9f9d78 100644 --- a/Core/Application/Common/Profiles/ApiDataMappingProfile.cs +++ b/Core/Application/Common/Profiles/ApiDataMappingProfile.cs @@ -40,6 +40,15 @@ namespace SecureCleanApiWaf.Core.Application.Common.Profiles /// public class ApiDataMappingProfile : Profile { + /// + /// Configures AutoMapper mappings used to translate between API DTOs and domain entities for API data items. + /// + /// + /// Sets up: + /// - Mapping from ApiItemDto to ApiDataItem, including field mappings and enrichment of the domain entity's metadata from DTO properties. + /// - Mapping from ApiDataItem to ApiDataItemDto, including extraction of metadata into DTO fields and computation of freshness/age values. + /// - Collection mappings for lists of ApiDataItem and conversion from ApiCollectionResponseDto<ApiItemDto> to List<ApiDataItem>. + /// public ApiDataMappingProfile() { // ===== API DTO to Domain Entity Mappings ===== @@ -154,4 +163,4 @@ public ApiDataMappingProfile() }); } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Commands/BlacklistTokenCommand.cs b/Core/Application/Features/Authentication/Commands/BlacklistTokenCommand.cs index ae07eb9..900fbb5 100644 --- a/Core/Application/Features/Authentication/Commands/BlacklistTokenCommand.cs +++ b/Core/Application/Features/Authentication/Commands/BlacklistTokenCommand.cs @@ -48,7 +48,14 @@ public class BlacklistTokenCommand : IRequest> /// The JWT token to blacklist /// Optional reason for blacklisting /// Client IP address for audit logging - /// Client user agent for audit logging + /// + /// Creates a command that requests blacklisting of the specified JWT token for logout/audit purposes. + /// + /// The JWT to blacklist. + /// Optional audit reason for the blacklist operation. + /// Optional client IP address for security logging. + /// Optional client user agent for security logging. + /// Thrown when is null. public BlacklistTokenCommand( string jwtToken, string? reason = null, @@ -106,4 +113,4 @@ public class BlacklistTokenResponse /// public string[] ClientRecommendations { get; set; } = Array.Empty(); } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Commands/BlacklistTokenCommandHandler.cs b/Core/Application/Features/Authentication/Commands/BlacklistTokenCommandHandler.cs index f28cb42..2327b27 100644 --- a/Core/Application/Features/Authentication/Commands/BlacklistTokenCommandHandler.cs +++ b/Core/Application/Features/Authentication/Commands/BlacklistTokenCommandHandler.cs @@ -35,7 +35,9 @@ public class BlacklistTokenCommandHandler : IRequestHandler /// Service for token blacklisting operations - /// Logger for audit and debugging + /// + /// Initializes a new instance of with its required dependencies. + /// public BlacklistTokenCommandHandler( ITokenBlacklistService tokenBlacklistService, ILogger logger) @@ -49,7 +51,12 @@ public BlacklistTokenCommandHandler( /// /// The blacklist token command /// Cancellation token - /// Result containing blacklist response or error information + /// + /// Processes a blacklist request for a JWT: validates the token, determines its state, and blacklists it if still valid. + /// + /// Command containing the JWT to blacklist and optional metadata (Reason, ClientIpAddress). + /// Token used to cancel the blacklist operation. + /// `Result` containing a `BlacklistTokenResponse` with token metadata and status on success, or error information on failure. public async Task> Handle(BlacklistTokenCommand request, CancellationToken cancellationToken) { try @@ -129,7 +136,11 @@ public async Task> Handle(BlacklistTokenCommand r /// Extracts key information from JWT token for processing. /// /// JWT token to parse - /// Token information including JTI, username, expiration, and validity + /// + /// Extracts the token identifier, username, and expiration time from the provided JWT string. + /// + /// The JWT compact serialization to parse. + /// A TokenInformation containing `Jti`, `Username`, `ExpiresAt`, and `IsValid`; if parsing fails, returns a TokenInformation with `IsValid` set to `false`. private TokenInformation ExtractTokenInformation(string jwtToken) { try @@ -169,7 +180,10 @@ private TokenInformation ExtractTokenInformation(string jwtToken) /// /// Gets standard client-side security recommendations. /// - /// Array of security recommendations for clients + /// + /// Client-side remediation steps to perform after a token is blacklisted or invalidated. + /// + /// An array of user-facing security recommendations (remove stored token, clear cached data, redirect to login, and similar actions). private static string[] GetClientRecommendations() { return new[] @@ -192,4 +206,4 @@ private class TokenInformation public bool IsValid { get; set; } } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Commands/LoginUserCommand.cs b/Core/Application/Features/Authentication/Commands/LoginUserCommand.cs index 10e559d..4040522 100644 --- a/Core/Application/Features/Authentication/Commands/LoginUserCommand.cs +++ b/Core/Application/Features/Authentication/Commands/LoginUserCommand.cs @@ -57,7 +57,15 @@ public class LoginUserCommand : IRequest> /// Password for authentication /// Requested role (User or Admin) /// Client IP address for audit logging - /// Client user agent for audit logging + /// + /// Creates a command to authenticate a user and initiate generation of a login response (token and claims). + /// + /// Username used for authentication (required). + /// Password used for authentication (required). + /// Requested user role; defaults to "User" when not provided. + /// Optional client IP address for audit and logging. + /// Optional client user agent string for audit and logging. + /// Thrown when or is null. public LoginUserCommand( string username, string password, @@ -72,4 +80,4 @@ public LoginUserCommand( UserAgent = userAgent; } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Commands/LoginUserCommandHandler.cs b/Core/Application/Features/Authentication/Commands/LoginUserCommandHandler.cs index c229f76..73f7609 100644 --- a/Core/Application/Features/Authentication/Commands/LoginUserCommandHandler.cs +++ b/Core/Application/Features/Authentication/Commands/LoginUserCommandHandler.cs @@ -64,7 +64,14 @@ public class LoginUserCommandHandler : IRequestHandlerRepository for user data access /// Repository for token data access /// JWT token generator service - /// Logger for audit and debugging + /// + /// Initializes a new instance of with the required repositories, token generator, and logger and validates that none are null. + /// + /// Repository for user persistence and retrieval. + /// Repository for persisting issued tokens. + /// Component that generates JWTs for authenticated users. + /// Logger for audit and diagnostic messages. + /// Thrown when , , , or is null. public LoginUserCommandHandler( IUserRepository userRepository, ITokenRepository tokenRepository, @@ -82,7 +89,11 @@ public LoginUserCommandHandler( /// /// The login command /// Cancellation token - /// Result containing login response or error information + /// + /// Handles a login request: authenticates the user, issues a JWT, records the token and login events, and returns login details and token metadata. + /// + /// LoginUserCommand containing username, password, optional requested role, client IP address, and user agent. + /// A Result<LoginResponseDto> containing token, expiry and user information on success; a failed Result with an error message on failure. public async Task> Handle(LoginUserCommand request, CancellationToken cancellationToken) { try @@ -225,7 +236,14 @@ public async Task> Handle(LoginUserCommand request, Can /// Extracts information from a generated JWT token. /// /// The JWT token to parse - /// Token information including JTI, issued at, and expiration + /// + /// Parses a JWT string and returns its identifier and timing metadata. + /// + /// The compact JWT to parse. + /// + /// A TokenInfo containing the token's JTI (if present), IssuedAt, and ExpiresAt. If parsing or claim conversion fails, + /// Jti will be a new GUID string and IssuedAt/ExpiresAt will use current UTC and current UTC + 30 minutes respectively. + /// private TokenInfo ExtractTokenInfo(string jwtToken) { try @@ -281,4 +299,4 @@ private class TokenInfo public DateTime ExpiresAt { get; set; } } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQuery.cs b/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQuery.cs index 29bde6b..eb7d2c8 100644 --- a/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQuery.cs +++ b/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQuery.cs @@ -55,10 +55,13 @@ public class GetTokenBlacklistStatsQuery : IRequest /// Initializes a new instance of GetTokenBlacklistStatsQuery. /// - /// Whether to bypass cache for real-time results + /// + /// Creates a query to request global token blacklist statistics, optionally bypassing cached results. + /// + /// If true, the query will bypass caching to obtain real-time statistics; otherwise cached results may be used. public GetTokenBlacklistStatsQuery(bool bypassCache = false) { BypassCache = bypassCache; } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQueryHandler.cs b/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQueryHandler.cs index 0e01ff6..adc12d8 100644 --- a/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQueryHandler.cs +++ b/Core/Application/Features/Authentication/Queries/GetTokenBlacklistStatsQueryHandler.cs @@ -36,7 +36,12 @@ public class GetTokenBlacklistStatsQueryHandler : IRequestHandler /// Service for token blacklist operations - /// Logger for audit and debugging + /// + /// Initializes a new instance of with the required dependencies. + /// + /// Service that provides base token blacklist statistics. + /// Logger used for diagnostic and error logging. + /// Thrown when or is null. public GetTokenBlacklistStatsQueryHandler( ITokenBlacklistService tokenBlacklistService, ILogger logger) @@ -50,7 +55,10 @@ public GetTokenBlacklistStatsQueryHandler( /// /// The statistics query /// Cancellation token - /// Result containing comprehensive statistics or error information + /// + /// Handles a GetTokenBlacklistStatsQuery by retrieving base blacklist statistics, enriching them with performance, security, and health metrics, and returning the assembled statistics DTO; on failure returns a safe fallback statistics DTO indicating an unhealthy/default state. + /// + /// A Result<TokenBlacklistStatisticsDto> containing the enriched statistics, or a fallback TokenBlacklistStatisticsDto representing an unhealthy/default state if an error occurred. public async Task> Handle(GetTokenBlacklistStatsQuery request, CancellationToken cancellationToken) { try @@ -84,7 +92,12 @@ public async Task> Handle(GetTokenBlacklistS /// /// Base statistics from the blacklist service /// Cancellation token - /// Enhanced statistics with additional monitoring data + /// + /// Assembles an enriched TokenBlacklistStatisticsDto by combining the provided base blacklist metrics with calculated performance, security, and health indicators. + /// + /// Basic token blacklist metrics retrieved from the blacklist service used as the source values. + /// Token to observe while performing asynchronous metric calculations. + /// A TokenBlacklistStatisticsDto containing the base metrics plus populated Performance, Security, and Health sections. private async Task CalculateEnhancedStatistics( TokenBlacklistStats baseStats, CancellationToken cancellationToken) @@ -114,7 +127,11 @@ private async Task CalculateEnhancedStatistics( /// Calculates performance-related metrics. /// /// Cancellation token - /// Performance metrics + /// + /// Builds performance metrics used to augment token blacklist statistics. + /// + /// Cancellation token to abort metric collection. + /// A PerformanceMetricsDto containing timings, operation counts, and cache hit rates. private async Task CalculatePerformanceMetrics(CancellationToken cancellationToken) { // In a real implementation, you would collect these metrics from: @@ -140,7 +157,10 @@ private async Task CalculatePerformanceMetrics(Cancellati /// Calculates security-related metrics. /// /// Cancellation token - /// Security metrics + /// + /// Produces security-related metrics used to populate the security section of the token blacklist statistics (current implementation returns simulated values). + /// + /// A SecurityMetricsDto containing counts of blocked attempts and suspicious patterns, an array of recent security event messages, and a mapping of top blocked IP addresses to their block counts. private async Task CalculateSecurityMetrics(CancellationToken cancellationToken) { // In a real implementation, you would collect these metrics from: @@ -174,7 +194,11 @@ private async Task CalculateSecurityMetrics(CancellationToke /// Calculates health indicators based on current system state. /// /// Current statistics - /// Health indicators + /// + /// Derives health indicators, warnings, and remediation recommendations from the provided token blacklist statistics. + /// + /// Current token blacklist statistics used to evaluate memory, cache, and security health. + /// A populated with status, warnings, and recommendations based on thresholds applied to the supplied statistics. private static HealthIndicatorsDto CalculateHealthIndicators(TokenBlacklistStatisticsDto stats) { var health = new HealthIndicatorsDto(); @@ -233,7 +257,13 @@ private static HealthIndicatorsDto CalculateHealthIndicators(TokenBlacklistStati /// /// Creates fallback statistics when the main service is unavailable. /// - /// Fallback statistics + /// + /// Create a safe fallback TokenBlacklistStatisticsDto used when the real statistics cannot be retrieved. + /// + /// + /// The returned DTO contains zeroed base metrics, empty performance and security sections, and an Unhealthy health indicator with a warning and a recommendation. + /// + /// A TokenBlacklistStatisticsDto with neutral metrics, empty performance and security data, and an Unhealthy health indicator. private static TokenBlacklistStatisticsDto CreateFallbackStatistics() { return new TokenBlacklistStatisticsDto @@ -268,4 +298,4 @@ private static TokenBlacklistStatisticsDto CreateFallbackStatistics() }; } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQuery.cs b/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQuery.cs index a79d11c..2f7d08f 100644 --- a/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQuery.cs +++ b/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQuery.cs @@ -60,7 +60,12 @@ public class IsTokenBlacklistedQuery : IRequest> /// Initializes a new instance of IsTokenBlacklistedQuery. /// /// JWT token to check - /// Whether to bypass cache for real-time results + /// + /// Creates a query that checks whether the provided JWT is blacklisted and prepares its cache metadata. + /// + /// The JWT to check for blacklist status; used to derive the cache key. + /// If true, instructs handlers to bypass cached results and obtain the latest status. + /// Thrown when is null. public IsTokenBlacklistedQuery(string jwtToken, bool bypassCache = false) { JwtToken = jwtToken ?? throw new ArgumentNullException(nameof(jwtToken)); @@ -74,7 +79,11 @@ public IsTokenBlacklistedQuery(string jwtToken, bool bypassCache = false) /// Generates a secure cache key from the JWT token. /// /// JWT token - /// Cache key for the token blacklist status + /// + /// Builds a cache key identifying the blacklist status for the specified JWT. + /// + /// The raw JWT string to derive the cache key from. + /// `TokenBlacklist:JTI:{jti}` if the token contains a JTI claim; otherwise `TokenBlacklist:Hash:{hash}` where `{hash}` is the token's GetHashCode(). private static string GenerateCacheKey(string token) { try @@ -99,4 +108,4 @@ private static string GenerateCacheKey(string token) return $"TokenBlacklist:Hash:{tokenHash}"; } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQueryHandler.cs b/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQueryHandler.cs index db5a808..aa74d53 100644 --- a/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQueryHandler.cs +++ b/Core/Application/Features/Authentication/Queries/IsTokenBlacklistedQueryHandler.cs @@ -37,7 +37,9 @@ public class IsTokenBlacklistedQueryHandler : IRequestHandler /// Service for token blacklist operations - /// Logger for audit and debugging + /// + /// Initializes a new instance of with required dependencies. + /// public IsTokenBlacklistedQueryHandler( ITokenBlacklistService tokenBlacklistService, ILogger logger) @@ -51,7 +53,11 @@ public IsTokenBlacklistedQueryHandler( /// /// The token blacklist query /// Cancellation token - /// Result containing blacklist status or error information + /// + /// Determines whether the provided JWT is blacklisted and returns a structured blacklist status. + /// + /// The query containing the JWT to check; if is null or empty the result will be an Invalid status indicating the token is required. + /// A Result containing a that indicates `Blacklisted`, `Valid`, or `Invalid`. If an internal error occurs the handler returns a `Valid` status with Details set to "Unable to verify blacklist status, assuming valid". public async Task> Handle(IsTokenBlacklistedQuery request, CancellationToken cancellationToken) { try @@ -128,7 +134,11 @@ public async Task> Handle(IsTokenBlacklistedQuer /// Extracts key information from JWT token for processing. /// /// JWT token to parse - /// Token information including JTI, expiration, and validity + /// + /// Parses a JWT string to extract the token identifier (JTI) and expiration and indicates whether the token could be parsed successfully. + /// + /// The JWT compact serialization to parse. + /// A TokenInformation containing the token's JTI, the expiration as a UTC DateTime (defaults to one hour from now if the `exp` claim is missing or cannot be parsed), and IsValid set to `true` when a JTI is present; on parse failure returns IsValid = `false`. private TokenInformation ExtractTokenInformation(string jwtToken) { try @@ -170,4 +180,4 @@ private class TokenInformation public bool IsValid { get; set; } } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/SampleData/Queries/GetApiDataByIdQuery.cs b/Core/Application/Features/SampleData/Queries/GetApiDataByIdQuery.cs index 74506bb..4fa4508 100644 --- a/Core/Application/Features/SampleData/Queries/GetApiDataByIdQuery.cs +++ b/Core/Application/Features/SampleData/Queries/GetApiDataByIdQuery.cs @@ -25,7 +25,12 @@ public class GetApiDataByIdQuery : IRequest>, ICacheable /// empty. /// The unique identifier of the resource to retrieve from the API. Cannot be null or empty. /// Specifies whether to bypass any cached data and force a fresh retrieval from the API. Set to to ignore cached results; otherwise, . + /// + /// Initializes a query to request an item of type from an external API by its identifier. + /// + /// The API endpoint path that identifies the resource collection or route. + /// The external API identifier of the resource to retrieve. + /// If , instructs handlers to bypass cached results and fetch fresh data; otherwise use cached data when available. public GetApiDataByIdQuery(string apiPath, string id, bool bypassCache = false) { ApiPath = apiPath; @@ -33,4 +38,4 @@ public GetApiDataByIdQuery(string apiPath, string id, bool bypassCache = false) BypassCache = bypassCache; } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/SampleData/Queries/GetApiDataByIdQueryHandler.cs b/Core/Application/Features/SampleData/Queries/GetApiDataByIdQueryHandler.cs index 6958c21..f1f2470 100644 --- a/Core/Application/Features/SampleData/Queries/GetApiDataByIdQueryHandler.cs +++ b/Core/Application/Features/SampleData/Queries/GetApiDataByIdQueryHandler.cs @@ -19,6 +19,7 @@ public class GetApiDataByIdQueryHandler : IRequestHandlerInitializes a new instance of GetApiDataByIdQueryHandler with the required API integration service. public GetApiDataByIdQueryHandler(IApiIntegrationService apiIntegrationService) { _apiService = apiIntegrationService; @@ -29,7 +30,11 @@ public GetApiDataByIdQueryHandler(IApiIntegrationService apiIntegrationService) /// /// The query containing the API path and the identifier of the data to retrieve. /// A cancellation token that can be used to cancel the asynchronous operation. - /// A result object containing the requested data if found; otherwise, a failure result with an error message. + /// + /// Handles a GetApiDataByIdQuery by retrieving the specified resource from the API and returning it wrapped in a Result. + /// + /// Query containing the API path and the resource ID to retrieve. + /// A Result<T> containing the fetched data when the API call succeeds; otherwise a failure Result with an error message. public async Task> Handle(GetApiDataByIdQuery request, CancellationToken cancellationToken) { try @@ -43,4 +48,4 @@ public async Task> Handle(GetApiDataByIdQuery request, Cancellation } } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/SampleData/Queries/GetApiDataQuery.cs b/Core/Application/Features/SampleData/Queries/GetApiDataQuery.cs index 4964b48..d1d76aa 100644 --- a/Core/Application/Features/SampleData/Queries/GetApiDataQuery.cs +++ b/Core/Application/Features/SampleData/Queries/GetApiDataQuery.cs @@ -29,11 +29,15 @@ public class GetApiDataQuery : IRequest>, ICacheable /// /// The URL of the API endpoint to query. Cannot be null or empty. /// Specifies whether to bypass any cached data and retrieve fresh results from the API. Set to to ignore cached responses; otherwise, . + /// + /// Initializes a new for the specified API endpoint. + /// + /// The base URL of the API endpoint to retrieve data from. + /// If , bypass cached responses and fetch fresh data; otherwise allow using cached results. public GetApiDataQuery(string apiUrl, bool bypassCache = false) { ApiUrl = apiUrl; BypassCache = bypassCache; } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/SampleData/Queries/GetApiDataQueryHandler.cs b/Core/Application/Features/SampleData/Queries/GetApiDataQueryHandler.cs index 0794440..04eaf93 100644 --- a/Core/Application/Features/SampleData/Queries/GetApiDataQueryHandler.cs +++ b/Core/Application/Features/SampleData/Queries/GetApiDataQueryHandler.cs @@ -68,7 +68,13 @@ public class GetApiDataQueryHandler : IRequestHandler, Res /// Repository for ApiDataItem persistence and retrieval. /// Service for external API integration operations. /// Mapper for converting API responses to domain entities. - /// Logger for monitoring and debugging. + /// + /// Initializes a new instance of with its required dependencies. + /// + /// Repository for persisting and retrieving ApiDataItem domain entities. + /// Service used to fetch data from the external API. + /// Mapper that converts external API responses into ApiDataItem entities. + /// Logger for the handler. public GetApiDataQueryHandler( IApiDataItemRepository repository, IApiIntegrationService apiService, @@ -86,7 +92,12 @@ public GetApiDataQueryHandler( /// /// The query containing the API URL for data retrieval. /// Cancellation token for async operation. - /// A task representing the async operation with Result containing the data. + /// + /// Handles a GetApiDataQuery by returning API data using a cache-first strategy and synchronizing repository state. + /// + /// The query containing the API URL to fetch and the requested response type. + /// A token to cancel the operation. + /// A Result<T> containing the mapped response data on success; a failure Result with an error message on failure. public async Task> Handle(GetApiDataQuery request, CancellationToken cancellationToken) { try @@ -229,7 +240,10 @@ public async Task> Handle(GetApiDataQuery request, CancellationToke /// Maps ApiDataItem entities to the expected response type T. /// /// List of domain entities. - /// Result with mapped response data. + /// + /// Map domain ApiDataItem entities to the requested response shape T (e.g., List<ApiDataItem>, IEnumerable<ApiDataItem>, object, or a compatible custom DTO). + /// + /// `Result` containing the mapped response value when mapping succeeds; a failed `Result` with an error message when mapping fails. private Result MapToResponseType(IReadOnlyList items) { try @@ -266,4 +280,4 @@ private Result MapToResponseType(IReadOnlyList items) } } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQuery.cs b/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQuery.cs index 9f689db..67e34e1 100644 --- a/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQuery.cs +++ b/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQuery.cs @@ -13,10 +13,16 @@ public class GetApiDataWithMappingQuery : IRequest>> public string ApiUrl { get; } public bool UseAutoMapper { get; } + /// + /// Initializes a new with the specified API URL and mapping preference. + /// + /// The target API URL to fetch data from. + /// If true, use AutoMapper for mapping; otherwise use an alternative/manual mapping strategy. + /// Thrown when is null. public GetApiDataWithMappingQuery(string apiUrl, bool useAutoMapper = true) { ApiUrl = apiUrl ?? throw new ArgumentNullException(nameof(apiUrl)); UseAutoMapper = useAutoMapper; } } -} +} \ No newline at end of file diff --git a/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQueryHandler.cs b/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQueryHandler.cs index 5535cb8..a478ce7 100644 --- a/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQueryHandler.cs +++ b/Core/Application/Features/SampleData/Queries/GetApiDataWithMappingQueryHandler.cs @@ -45,6 +45,10 @@ public class GetApiDataWithMappingQueryHandler : IRequestHandler + /// Initializes a new instance of the handler and validates that all injected dependencies are not null. + /// + /// Thrown when any required dependency (repository, apiService, autoMapper, customMapper, or logger) is null. public GetApiDataWithMappingQueryHandler( IApiDataItemRepository repository, IApiIntegrationService apiService, @@ -59,6 +63,11 @@ public GetApiDataWithMappingQueryHandler( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// + /// Retrieves API data for the specified URL using cache when fresh; otherwise fetches from the external API and maps results using either AutoMapper (for known structures) or a custom dynamic mapper, synchronizes the repository, and returns DTOs. + /// + /// Query containing the API URL and a flag indicating whether to use AutoMapper for mapping known API structures. + /// `Result>` containing the mapped list of API data items on success; on failure the result contains an error message. If the external API call fails but cached data exists, returns stale cached DTOs instead of an error. public async Task>> Handle( GetApiDataWithMappingQuery request, CancellationToken cancellationToken) @@ -151,6 +160,12 @@ public async Task>> Handle( } } + /// + /// Handle an API fetch failure by returning stale cached items when available, otherwise a failure result. + /// + /// Error message describing the API fetch failure. + /// Cached entities retrieved from the repository for the requested API URL. + /// `Ok` result containing DTOs for cached items whose status is not Deleted if any exist; otherwise a `Fail` result containing the provided error message. private Result> HandleApiFailure(string error, IReadOnlyList cachedItems) { _logger.LogWarning("API fetch failed: {Error}", error); @@ -168,6 +183,16 @@ private Result> HandleApiFailure(string error, IReadOnlyLis return Result>.Fail(error); } + /// + /// Synchronizes freshly mapped ApiDataItem entities with the repository and persists the resulting changes. + /// + /// Entities produced from the external API to be inserted or used to update existing records. + /// Cached items previously retrieved for the same API URL (provided for context; not modified by this method). + /// Token to observe while performing repository operations. + /// + /// For each item in , the method adds it when no repository record exists with the same external ID; + /// otherwise it updates the existing record's name and description, replaces its metadata with the fresh item's metadata, and saves all changes. + /// private async Task SyncWithRepository( List freshEntities, IReadOnlyList cachedItems, @@ -197,4 +222,4 @@ private async Task SyncWithRepository( await _repository.SaveChangesAsync(cancellationToken); } } -} +} \ No newline at end of file diff --git a/Core/Domain/Entities/ApiDataItem.cs b/Core/Domain/Entities/ApiDataItem.cs index 7dc7341..876579f 100644 --- a/Core/Domain/Entities/ApiDataItem.cs +++ b/Core/Domain/Entities/ApiDataItem.cs @@ -114,7 +114,9 @@ namespace SecureCleanApiWaf.Core.Domain.Entities /// /// Private constructor for EF Core. Prevents direct instantiation. It is used by the static factory method called CreateFromExternalSource to create new instances. - /// + /// +/// Parameterless constructor required by Entity Framework Core for materialization. +/// private ApiDataItem() { } /// @@ -125,7 +127,15 @@ private ApiDataItem() { } /// The item description. /// The API endpoint URL. /// A new ApiDataItem entity. - /// Thrown when validation fails. + /// + /// Create a new ApiDataItem from external source data. + /// + /// External system identifier; must be non-empty. + /// Item name; must be non-empty and at most 500 characters. + /// Optional item description; if provided, must be at most 2000 characters. + /// Absolute URL of the external source; must be a valid absolute URI. + /// A new ApiDataItem initialized with the provided values, Status set to Active, and timestamps (LastSyncedAt and CreatedAt) set to UTC now. + /// Thrown when any validation rule for the inputs is violated. public static ApiDataItem CreateFromExternalSource( string externalId, string name, @@ -172,7 +182,16 @@ public static ApiDataItem CreateFromExternalSource( /// /// Updated name. /// Updated description. - /// Thrown when validation fails. + /// + /// Update the item's name and description from an external source and mark the item as freshly synced. + /// + /// New display name; must be non-empty and no more than 500 characters. + /// New description text; optional. If provided, must be no more than 2000 characters. + /// + /// Sets , , updates and to UtcNow, and sets to Active. + /// + /// Thrown when any input validation fails (empty name or length constraints). + /// Thrown when the item is in Deleted status and therefore cannot be updated. public void UpdateFromExternalSource(string name, string description) { // Validation: Name @@ -207,6 +226,12 @@ public void UpdateFromExternalSource(string name, string description) /// /// Called when data exceeds freshness threshold. /// Stale data can still be served while refresh happens in background. + /// + /// Marks the item as stale to indicate its external data is out-of-date. + /// + /// + /// If the item is already marked as stale or is deleted, the method does nothing. + /// Otherwise it sets the Status to and updates the entity's UpdatedAt timestamp. /// public void MarkAsStale() { @@ -222,7 +247,13 @@ public void MarkAsStale() /// /// Marks the data as active (fresh and valid). + /// + /// Marks the data item as active. /// + /// + /// Sets to and updates and to the current UTC time. + /// + /// Thrown if the item is deleted; deleted items must be restored before reactivation. public void MarkAsActive() { if (Status == DataStatus.Deleted) @@ -240,7 +271,14 @@ public void MarkAsActive() /// /// Marks the data as deleted (soft delete). /// - /// Reason for deletion (for audit). + /// + /// Marks the item as deleted and records the deletion reason for audit. + /// + /// Non-empty reason for the deletion; persisted to metadata for auditing. + /// Thrown when is null, empty, or whitespace. + /// + /// If the item is already deleted this method does nothing. When executed it sets the status to Deleted, performs a soft delete, updates the UpdatedAt timestamp, and stores the keys "deletion_reason" and "deleted_by" in metadata. + /// public void MarkAsDeleted(string reason) { if (string.IsNullOrWhiteSpace(reason)) @@ -263,7 +301,13 @@ public void MarkAsDeleted(string reason) /// /// /// Restored data is marked as stale and should be refreshed. + /// + /// Restores an entity that was previously soft-deleted, marking it as needing refresh. + /// + /// + /// Sets to and updates to the current UTC time. /// + /// Thrown if the entity is not currently deleted. public new void Restore() { if (Status != DataStatus.Deleted) @@ -295,7 +339,11 @@ public void MarkAsDeleted(string reason) /// await RefreshDataAsync(item); /// } /// ``` - /// + /// + /// Determines whether the item should be refreshed based on a maximum allowed age. + /// + /// Maximum allowed age before a refresh is required. + /// `true` if the time since is greater than and the item is not deleted, `false` otherwise. public bool NeedsRefresh(TimeSpan maxAge) { if (Status == DataStatus.Deleted) @@ -308,7 +356,10 @@ public bool NeedsRefresh(TimeSpan maxAge) /// /// Gets the age of the data (time since last sync). /// - /// TimeSpan representing data age. + /// + /// Gets the time elapsed since the item was last synchronized. + /// + /// The duration since LastSyncedAt (UTC) as a . public TimeSpan GetAge() { return DateTime.UtcNow - LastSyncedAt; @@ -318,7 +369,11 @@ public TimeSpan GetAge() /// Checks if the data is fresh based on a threshold. /// /// Threshold for considering data fresh. - /// True if data is fresh, false otherwise. + /// + /// Determines whether the item is considered fresh compared to the provided freshness threshold. + /// + /// Maximum allowed age for the item's data to be considered fresh. + /// true if the item is fresh, false otherwise. public bool IsFresh(TimeSpan freshnessThreshold) { if (Status != DataStatus.Active) @@ -332,7 +387,12 @@ public bool IsFresh(TimeSpan freshnessThreshold) /// /// Metadata key. /// Metadata value. - /// Thrown when key is invalid. + /// + /// Adds or updates a metadata entry for the item. + /// + /// The metadata key; must be non-empty and not whitespace. + /// The metadata value; must not be null. + /// Thrown when is empty/whitespace or is null. public void AddMetadata(string key, object value) { if (string.IsNullOrWhiteSpace(key)) @@ -349,7 +409,11 @@ public void AddMetadata(string key, object value) /// Removes a metadata entry. /// /// Metadata key to remove. - /// True if removed, false if key didn't exist. + /// + /// Removes the metadata entry with the specified key if it exists. + /// + /// The metadata key to remove. + /// true if the key existed and was removed, false otherwise. public bool RemoveMetadata(string key) { if (string.IsNullOrWhiteSpace(key)) @@ -370,7 +434,11 @@ public bool RemoveMetadata(string key) /// /// Expected type of the value. /// Metadata key. - /// The metadata value, or default(T) if not found. + /// + /// Retrieve a metadata entry by key and return it as the requested type. + /// + /// The metadata key to look up; must be non-empty and non-whitespace. + /// The metadata value cast to `T`, or `default(T)` if the key is missing, the key is whitespace, or the value cannot be cast to `T`. public T? GetMetadata(string key) { if (string.IsNullOrWhiteSpace(key)) @@ -393,7 +461,11 @@ public bool RemoveMetadata(string key) /// Checks if metadata contains a specific key. /// /// The key to check. - /// True if key exists, false otherwise. + /// + /// Checks whether a metadata entry exists for the specified key. + /// + /// The metadata key to check; empty or whitespace keys are treated as absent. + /// `true` if a metadata entry exists for the specified key and the key is not empty or whitespace, `false` otherwise. public bool HasMetadata(string key) { return !string.IsNullOrWhiteSpace(key) && _metadata.ContainsKey(key); @@ -401,6 +473,8 @@ public bool HasMetadata(string key) /// /// Clears all metadata. + /// + /// Removes all metadata entries and updates the entity's UpdatedAt timestamp. /// public void ClearMetadata() { @@ -412,7 +486,12 @@ public void ClearMetadata() /// Updates the source URL (if API endpoint changes). /// /// The new source URL. - /// Thrown when URL is invalid. + /// + /// Update the entity's source URL after validating it is non-empty and an absolute URL. + /// + /// The new absolute source URL to assign; must be a non-empty, valid absolute URL. + /// Thrown when the provided URL is empty or not a valid absolute URL. + /// Sets the SourceUrl property and updates UpdatedAt to the current UTC time. public void UpdateSourceUrl(string newSourceUrl) { if (string.IsNullOrWhiteSpace(newSourceUrl)) @@ -428,7 +507,10 @@ public void UpdateSourceUrl(string newSourceUrl) /// /// Gets a summary of the data item for logging/debugging. /// - /// Data item summary string. + /// + /// Builds a one-line human-readable summary of the item's identity, status, synchronization time, age, and metadata count. + /// + /// A formatted summary string containing the Id, ExternalId, Name, Status, LastSyncedAt (formatted as yyyy-MM-dd HH:mm:ss), age in minutes with one decimal, and the count of metadata items. public string GetSummary() { return $"ID: {Id}, " + @@ -440,4 +522,4 @@ public string GetSummary() $"Metadata: {Metadata.Count} items"; } } -} +} \ No newline at end of file diff --git a/Core/Domain/Entities/BaseEntity.cs b/Core/Domain/Entities/BaseEntity.cs index 4e1d5ce..da5daec 100644 --- a/Core/Domain/Entities/BaseEntity.cs +++ b/Core/Domain/Entities/BaseEntity.cs @@ -120,6 +120,11 @@ public abstract class BaseEntity /// Implementation Note: /// Infrastructure layer should configure global query filters /// to automatically exclude soft-deleted entities from queries. + /// + /// Marks the entity as soft-deleted. + /// + /// + /// Sets to true and records and with the current UTC time. /// public void SoftDelete() { @@ -145,6 +150,11 @@ public void SoftDelete() /// /// Note: Only works for soft-deleted entities. /// Hard-deleted (physically removed) entities cannot be restored. + /// + /// Restores a soft-deleted entity by clearing its deletion state and updating its last-modified time. + /// + /// + /// Sets to false, clears , and sets to the current UTC time. /// public void Restore() { @@ -165,7 +175,10 @@ public void Restore() /// /// This follows Domain-Driven Design (DDD) principles where /// entities are defined by their identity, not their attributes. - /// + /// + /// Determines whether the specified object represents the same domain entity by comparing concrete types and non-empty identifiers. + /// + /// `true` if is a BaseEntity of the same concrete type and both entities have the same non-empty Id; `false` otherwise. public override bool Equals(object? obj) { if (obj is not BaseEntity other) @@ -190,7 +203,10 @@ public override bool Equals(object? obj) /// /// Hash code is based on the entity's ID. /// This ensures consistent hashing for entities with the same ID. - /// + /// + /// Gets the hash code for the entity derived from its Id. + /// + /// The hash code computed from the entity's Id. public override int GetHashCode() { return Id.GetHashCode(); @@ -218,4 +234,4 @@ public override int GetHashCode() return !(left == right); } } -} +} \ No newline at end of file diff --git a/Core/Domain/Entities/Token.cs b/Core/Domain/Entities/Token.cs index 1686b4a..e3537d5 100644 --- a/Core/Domain/Entities/Token.cs +++ b/Core/Domain/Entities/Token.cs @@ -145,7 +145,12 @@ namespace SecureCleanApiWaf.Core.Domain.Entities /// /// Private constructor for EF Core. - /// + /// +/// Parameterless constructor used by Entity Framework Core and other ORMs for object materialization. +/// +/// +/// Intended for framework use only; application code should not call this constructor directly. +/// private Token() { } /// @@ -158,7 +163,18 @@ private Token() { } /// Optional client IP address. /// Optional user agent string. /// A new Token entity. - /// Thrown when validation fails. + /// + /// Creates a new Token aggregate with validated metadata and initial Active status. + /// + /// The JWT ID (`jti`) that uniquely identifies the token; required and non-empty. + /// Identifier of the user who owns the token; required and not Guid.Empty. + /// User name for audit/logging; required and non-empty. + /// Token expiration time; must be in the future. Access tokens cannot exceed 2 hours and refresh tokens cannot exceed 90 days from now. + /// The token type (e.g., AccessToken or RefreshToken) which influences lifetime rules. + /// Optional client IP address associated with the token issuance. + /// Optional client user agent associated with the token issuance. + /// The newly created Token initialized with IssuedAt, ExpiresAt, Status = Active, CreatedAt, and a new Id. + /// Thrown when any validation or business rule (empty values, expiration in the past, or lifetime limits) fails. public static Token Create(string tokenId, Guid userId, string username, DateTime expiresAt, @@ -232,7 +248,12 @@ public static Token Create(string tokenId, Guid userId, /// - Creating security audit logs /// - Notifying users of unexpected revocations /// - Analytics and security monitoring - /// + /// + /// Revokes the token, marking it as invalid and recording revocation metadata. + /// + /// Non-empty justification for the revocation, recorded for audit. + /// Thrown when is null, empty, or whitespace. + /// Thrown when the token is already revoked or already expired. public void Revoke(string reason) { if (string.IsNullOrWhiteSpace(reason)) @@ -271,7 +292,14 @@ public void Revoke(string reason) /// /// Called by background jobs or when checking token validity. /// Tokens automatically expire based on ExpiresAt timestamp. + /// + /// Transition the token to the Expired status when its expiration time has been reached. + /// + /// + /// If the token is already Expired or has been Revoked, the method does nothing. + /// Otherwise, it sets the token's Status to Expired and updates UpdatedAt to the current UTC time. /// + /// Thrown when the token's ExpiresAt is in the future and therefore not yet eligible to be marked expired. public void MarkAsExpired() { if (Status == TokenStatus.Expired) @@ -301,7 +329,10 @@ public void MarkAsExpired() /// - Not expired /// - Not revoked /// - Not soft-deleted - /// + /// + /// Determines whether the token is currently valid for use in authentication checks. + /// + /// `true` if the token's status is Active, it is not expired, and it is not soft-deleted; `false` otherwise. public bool IsValid() { return Status == TokenStatus.Active @@ -312,7 +343,10 @@ public bool IsValid() /// /// Checks if the token has expired. /// - /// True if expired, false otherwise. + /// + /// Determines whether the token has passed its expiration time. + /// + /// `true` if `ExpiresAt` is less than or equal to the current UTC time, `false` otherwise. public bool IsExpired() { return ExpiresAt <= DateTime.UtcNow; @@ -321,7 +355,10 @@ public bool IsExpired() /// /// Checks if the token is revoked. /// - /// True if revoked, false otherwise. + /// + /// Determines whether the token has been revoked. + /// + /// `true` if the token's status is Revoked, `false` otherwise. public bool IsRevoked() { return Status == TokenStatus.Revoked; @@ -342,7 +379,10 @@ public bool IsRevoked() /// // Refresh token soon /// } /// ``` - /// + /// + /// Gets the remaining lifetime of the token measured against the current UTC time. + /// + /// The remaining time until the token's expiration; `TimeSpan.Zero` if the token is already expired. public TimeSpan GetRemainingLifetime() { if (IsExpired()) @@ -363,7 +403,10 @@ public TimeSpan GetRemainingLifetime() /// - Not revoked /// /// Access tokens cannot be refreshed directly. - /// + /// + /// Determines whether the token is eligible to be used to obtain a new access token via refresh. + /// + /// true if the token's type is RefreshToken, its status is Active, it is not expired, and it is not revoked; false otherwise. public bool CanBeRefreshed() { return Type == TokenType.RefreshToken @@ -387,7 +430,11 @@ public bool CanBeRefreshed() /// var newToken = await RefreshTokenAsync(token); /// } /// ``` - /// + /// + /// Determines whether the token's remaining lifetime is within a short expiration window. + /// + /// Optional time window to consider "expiring soon"; defaults to 5 minutes. + /// `true` if the token has remaining lifetime greater than zero and less than or equal to the threshold, `false` otherwise. public bool IsExpiringSoon(TimeSpan? threshold = null) { var checkThreshold = threshold ?? TimeSpan.FromMinutes(5); @@ -399,7 +446,10 @@ public bool IsExpiringSoon(TimeSpan? threshold = null) /// /// Gets the token age (time since issued). /// - /// TimeSpan representing token age. + /// + /// Gets the amount of time that has elapsed since the token was issued. + /// + /// The duration of time between the token's IssuedAt timestamp and now. public TimeSpan GetAge() { return DateTime.UtcNow - IssuedAt; @@ -409,7 +459,11 @@ public TimeSpan GetAge() /// Checks if the token belongs to a specific user. /// /// The user ID to check. - /// True if token belongs to user, false otherwise. + /// + /// Determines whether the token belongs to the specified user. + /// + /// The user identifier to compare against the token's owner. + /// true if the token's equals the provided , false otherwise. public bool BelongsToUser(Guid userId) { return UserId == userId; @@ -432,7 +486,11 @@ public bool BelongsToUser(Guid userId) /// _logger.LogWarning("Token used from different IP"); /// } /// ``` - /// + /// + /// Determines whether the provided IP address matches the token's recorded client IP or is allowed when no client IP is recorded. + /// + /// The IP address to check; may be null or empty. + /// True if the token has no recorded client IP or the provided IP matches the recorded client IP (comparison is case-insensitive); false otherwise. public bool IsFromIpAddress(string? ipAddress) { if (string.IsNullOrWhiteSpace(ClientIpAddress)) @@ -448,7 +506,10 @@ public bool IsFromIpAddress(string? ipAddress) /// /// Never includes the actual JWT token value. /// Safe for logging and debugging purposes. - /// + /// + /// Produces a compact, human-readable summary of the token's key metadata. + /// + /// A single-line string containing TokenId, Username, Type, Status, IssuedAt, ExpiresAt, and whether the token is currently valid. public string GetSummary() { return $"TokenID: {TokenId}, " + @@ -478,10 +539,15 @@ public string GetSummary() /// /// token.ClearDomainEvents(); /// + /// + /// Clears all domain events recorded by this aggregate. + /// + /// + /// Call after domain events have been published externally to remove them from the internal collection; no-op if there are none. /// public void ClearDomainEvents() { _domainEvents.Clear(); } } -} +} \ No newline at end of file diff --git a/Core/Domain/Entities/User.cs b/Core/Domain/Entities/User.cs index e73b495..4029b86 100644 --- a/Core/Domain/Entities/User.cs +++ b/Core/Domain/Entities/User.cs @@ -140,7 +140,9 @@ namespace SecureCleanApiWaf.Core.Domain.Entities /// /// Private constructor for EF Core. - /// + /// +/// Initializes a new instance of the class for use by Entity Framework Core and other ORMs. +/// private User() { } /// @@ -162,7 +164,19 @@ private User() { } /// - Initializing user preferences /// - Integrating with external systems (CRM, analytics) /// - //3. Factory method to create new User instances with validation and event raising + /// + /// Create a new active User with the provided credentials, assign the default User role, and enqueue a UserRegisteredEvent. + /// + /// Desired username (3–50 characters; letters, digits, '.', '_', and '-' only). + /// Verified email value object for the user. + /// Non-empty hashed password for the user. + /// Optional IP address associated with the registration. + /// Optional user agent string associated with the registration. + /// Non-empty identifier of how the user registered (e.g., "Email"). + /// The newly created User instance. + /// Thrown when any validation fails: + /// username is empty, too short (<3) or too long (>50), contains invalid characters; + /// email is null; passwordHash is empty; or registrationMethod is empty. public static User Create( string username, Email email, @@ -229,7 +243,12 @@ public static User Create( /// /// The role to assign. /// Thrown when role assignment violates business rules. - // 4. Business methods enforcing rules and updating state + /// + /// Assigns the specified role to the user if the user's status allows role changes. + /// + /// The role to assign; must not be null. If the user already has this role, the call is ignored. + /// Thrown when is null. + /// Thrown when the user's current status does not permit assigning roles (for example, when suspended or locked). public void AssignRole(Role role) { if (role == null) @@ -254,7 +273,12 @@ public void AssignRole(Role role) /// Removes a role from the user. /// /// The role to remove. - /// Thrown when removal violates business rules. + /// + /// Removes the specified role from the user while enforcing that the user retains at least one role. + /// + /// The role to remove. + /// Thrown when is null. + /// Thrown when removing the role would leave the user with no roles. public void RemoveRole(Role role) { if (role == null) @@ -278,7 +302,12 @@ public void RemoveRole(Role role) /// Records a successful login attempt. /// /// The IP address of the login. - /// The user agent string. + /// + /// Record a successful login and update the user's last-login tracking and authentication state. + /// + /// The client's IP address, or null if not available. + /// The client's user-agent string, or null if not available. + /// Thrown if the user is not allowed to log in (e.g., not active or otherwise blocked). public void RecordLogin(string? ipAddress, string? userAgent) { // Business Rule: Can only login if active @@ -301,7 +330,11 @@ public void RecordLogin(string? ipAddress, string? userAgent) /// Records a failed login attempt and locks account if threshold exceeded. /// /// Maximum allowed failed attempts before locking (default: 5). - /// Duration of lockout (default: 15 minutes). + /// + /// Records a failed login attempt for the user and locks the account when the configured threshold is reached. + /// + /// Number of failed attempts required to trigger an account lock (default: 5). + /// Duration to lock the account when the threshold is reached (default: 15 minutes; null means a permanent lock). public void RecordFailedLogin(int maxAttempts = 5, TimeSpan? lockoutDuration = null) { FailedLoginAttempts++; @@ -317,7 +350,16 @@ public void RecordFailedLogin(int maxAttempts = 5, TimeSpan? lockoutDuration = n /// /// Deactivates the user account (voluntary user action). + /// + /// Mark the user account as inactive. /// + /// + /// This operation is idempotent: if the account is already inactive it does nothing. + /// On success it sets the user's status to and updates the timestamp. + /// + /// + /// Thrown when the current user status is not (and not already inactive). + /// public void Deactivate() { if (Status == UserStatus.Inactive) @@ -334,9 +376,15 @@ public void Deactivate() UpdatedAt = DateTime.UtcNow; } + /// + /// Activates the user account. /// /// Activates the user account. /// + /// + /// No-op if the account is already active. Throws when the account is suspended and requires admin approval to reactivate. On success, sets the status to Active, resets failed login attempts, clears any temporary lock, and updates the UpdatedAt timestamp. + /// + /// Thrown when attempting to activate a suspended account. public void Activate() { if (Status == UserStatus.Active) @@ -359,7 +407,14 @@ public void Activate() /// /// Suspends the user account (admin action). /// - /// Reason for suspension (required for audit). + /// + /// Suspends the user account and records an audit reason. + /// + /// + /// No-op if the user is already suspended. + /// + /// Non-empty reason for suspension used for auditing. + /// Thrown when is null, empty, or whitespace. public void Suspend(string reason) { if (string.IsNullOrWhiteSpace(reason)) @@ -379,7 +434,12 @@ public void Suspend(string reason) /// Locks the user account due to security concerns. /// /// Reason for locking. - /// Optional lock duration (permanent if null). + /// + /// Locks the user account with a specified reason and optional duration. + /// + /// A non-empty explanation for the lock; required. + /// Optional lock duration; when null the lock is permanent. + /// Thrown when is null, empty, or whitespace. public void Lock(string reason, TimeSpan? duration = null) { if (string.IsNullOrWhiteSpace(reason)) @@ -395,7 +455,10 @@ public void Lock(string reason, TimeSpan? duration = null) /// /// Unlocks a locked user account (admin action). + /// + /// Unlocks a locked user account, sets its status to Active, resets failed login attempts, clears any temporary lock expiration, and updates the modification timestamp. /// + /// Thrown if the account is not currently in the Locked status. public void Unlock() { if (Status != UserStatus.Locked) @@ -414,7 +477,11 @@ public void Unlock() /// /// Updates the user's email address. /// - /// The new email address. + /// + /// Replaces the user's email with the provided value and updates the entity's timestamp. + /// + /// The new email value object to set. + /// Thrown if is null. public void UpdateEmail(Email newEmail) { if (newEmail == null) @@ -430,7 +497,11 @@ public void UpdateEmail(Email newEmail) /// /// Updates the user's password hash. /// - /// The new hashed password. + /// + /// Update the user's stored password hash and reset related authentication state. + /// + /// The new password hash (must be a non-empty hashed value). + /// Thrown when is null, empty, or whitespace. public void UpdatePassword(string newPasswordHash) { if (string.IsNullOrWhiteSpace(newPasswordHash)) @@ -447,13 +518,19 @@ public void UpdatePassword(string newPasswordHash) /// /// Checks if the user account is currently active. /// - /// True if status is Active, false otherwise. + /// +/// Determines whether the user is currently active. +/// +/// `true` if the user's status is Active, `false` otherwise. public bool IsActive() => Status == UserStatus.Active; /// /// Checks if the user can log in. /// - /// True if login is allowed, false otherwise. + /// + /// Determines whether the user is permitted to log in given their current status and lock state. + /// + /// `true` if the user is not deleted and either has status `Active` or had a temporary `Locked` status that has expired; `false` otherwise. public bool CanLogin() { // Cannot login if account is deleted @@ -481,7 +558,11 @@ public bool CanLogin() /// Checks if the user has a specific role. /// /// The role to check. - /// True if user has the role, false otherwise. + /// + /// Determines whether the user has the specified role. + /// + /// The role to check; if null, it is treated as not present. + /// `true` if the user has the specified role, `false` otherwise. public bool HasRole(Role role) { return role != null && _roles.Contains(role); @@ -490,7 +571,10 @@ public bool HasRole(Role role) /// /// Checks if the user has any admin privileges. /// - /// True if user has Admin or SuperAdmin role, false otherwise. + /// + /// Determines whether the user has an administrative role. + /// + /// `true` if the user has an administrative role (Admin or SuperAdmin), `false` otherwise. public bool IsAdmin() { return _roles.Any(r => r.IsAdmin()); @@ -499,7 +583,10 @@ public bool IsAdmin() /// /// Checks if the user has SuperAdmin privileges. /// - /// True if user has SuperAdmin role, false otherwise. + /// + /// Determines whether the user has the SuperAdmin role. + /// + /// `true` if the user has the SuperAdmin role, `false` otherwise. public bool IsSuperAdmin() { return _roles.Any(r => r.IsSuperAdmin()); @@ -509,7 +596,10 @@ public bool IsSuperAdmin() /// Validates username format. /// /// Username to validate. - /// True if valid format, false otherwise. + /// + /// Determines whether a username contains only allowed characters. + /// + /// `true` if the username consists exclusively of letters, digits, '.', '_', or '-', `false` otherwise. private static bool IsValidUsername(string username) { // Allow: letters, numbers, dots, underscores, hyphens @@ -539,10 +629,15 @@ private static bool IsValidUsername(string username) /// /// user.ClearDomainEvents(); /// + /// + /// Removes all domain events accumulated by the aggregate. + /// + /// + /// Call after domain events have been published to prevent re-publishing. /// public void ClearDomainEvents() { _domainEvents.Clear(); } } -} +} \ No newline at end of file diff --git a/Core/Domain/Events/BaseDomainEvent.cs b/Core/Domain/Events/BaseDomainEvent.cs index c95e322..a7df3f7 100644 --- a/Core/Domain/Events/BaseDomainEvent.cs +++ b/Core/Domain/Events/BaseDomainEvent.cs @@ -227,10 +227,16 @@ public abstract class BaseDomainEvent : IDomainEvent /// /// These values are immutable and cannot be changed after creation, /// ensuring event integrity and consistency. + /// + /// Initializes a new BaseDomainEvent with a unique event identifier and a UTC occurrence timestamp. + /// + /// + /// Sets to a newly generated GUID and to the current UTC time. + /// These properties are init-only to preserve event immutability and represent the event's creation moment (not processing time). /// protected BaseDomainEvent() { EventId = Guid.NewGuid(); OccurredOn = DateTime.UtcNow; } -} +} \ No newline at end of file diff --git a/Core/Domain/Events/TokenRevokedEvent.cs b/Core/Domain/Events/TokenRevokedEvent.cs index 9cb9544..5549d93 100644 --- a/Core/Domain/Events/TokenRevokedEvent.cs +++ b/Core/Domain/Events/TokenRevokedEvent.cs @@ -350,7 +350,17 @@ public class TokenRevokedEvent : BaseDomainEvent /// /// /// Thrown when or is empty GUID. - /// + /// + /// Creates a TokenRevokedEvent capturing details about a JWT token that was explicitly revoked before its expiration. + /// + /// The token's unique identifier (JTI) used as the blacklist key. + /// The unique identifier of the user who owned the token. + /// The username of the user at the time of revocation. + /// The type of token that was revoked (e.g., AccessToken or RefreshToken). + /// The original expiration time of the token. + /// A human-readable reason for the revocation. + /// Thrown when or is . + /// Thrown when or is null, empty, or whitespace. public TokenRevokedEvent( Guid tokenId, Guid userId, @@ -378,4 +388,4 @@ public TokenRevokedEvent( ExpiresAt = expiresAt; Reason = reason; } -} +} \ No newline at end of file diff --git a/Core/Domain/Events/UserRegisteredEvent.cs b/Core/Domain/Events/UserRegisteredEvent.cs index b4057ef..f513754 100644 --- a/Core/Domain/Events/UserRegisteredEvent.cs +++ b/Core/Domain/Events/UserRegisteredEvent.cs @@ -490,7 +490,18 @@ public class UserRegisteredEvent : BaseDomainEvent /// /// Thrown when , , /// , or is null or empty. - /// + /// + /// Initializes a domain event representing a newly registered user and the details of their registration. + /// + /// Unique identifier of the newly registered user. + /// Chosen username for the user. + /// Email address value object for the user. + /// Read-only list of roles assigned at registration; must contain at least one role. + /// Optional IP address from which the registration originated. + /// Optional User-Agent string from the registration request. + /// Method or source of registration (for example, "Email" or "Google"). + /// Thrown when is . + /// Thrown when , , , or is null or invalid (username/registrationMethod empty or whitespace, or empty). public UserRegisteredEvent( Guid userId, string username, @@ -522,4 +533,4 @@ public UserRegisteredEvent( UserAgent = userAgent; RegistrationMethod = registrationMethod; } -} +} \ No newline at end of file diff --git a/Core/Domain/Exceptions/DomainException.cs b/Core/Domain/Exceptions/DomainException.cs index ecf24a4..294ff54 100644 --- a/Core/Domain/Exceptions/DomainException.cs +++ b/Core/Domain/Exceptions/DomainException.cs @@ -124,7 +124,10 @@ public class DomainException : Exception /// - Written in business language /// - Safe to display to users (no technical details) /// - Actionable (user knows what to fix) - /// + /// + /// Initializes a new DomainException with a descriptive business-rule violation message. + /// + /// A user-facing message describing the domain rule violation. public DomainException(string message) : base(message) { } @@ -148,7 +151,11 @@ public DomainException(string message) : base(message) /// throw new DomainException("Invalid email format", ex); /// } /// ``` - /// + /// + /// Initializes a new DomainException with a descriptive message and an underlying cause. + /// + /// A descriptive, user-facing message that explains the domain rule violation. + /// The underlying exception that caused this domain exception, if any. public DomainException(string message, Exception innerException) : base(message, innerException) { @@ -191,7 +198,11 @@ public class EntityNotFoundException : DomainException /// Initializes a new instance of the class. /// /// The name of the entity type (e.g., "User", "Order"). - /// The identifier of the entity that was not found. + /// + /// Initializes a new for a missing entity and composes a descriptive message. + /// + /// The type or name of the entity that could not be located. + /// The identifier value of the missing entity. public EntityNotFoundException(string entityName, object entityId) : base($"{entityName} with ID '{entityId}' was not found") { @@ -202,7 +213,11 @@ public EntityNotFoundException(string entityName, object entityId) /// /// Initializes a new instance of the class with a custom message. /// - /// Custom error message. + /// + /// Initializes a new instance of with a custom message. + /// + /// Custom error message describing the not-found condition. + /// When constructed through this overload, and are initialized to empty strings. public EntityNotFoundException(string message) : base(message) { @@ -241,11 +256,16 @@ public class InvalidDomainOperationException : DomainException /// Initializes a new instance of the class. /// /// The operation that was attempted. + /// + /// Initializes a new for an attempted operation that is invalid for the specified reason. + /// + /// The name or description of the operation that was attempted. /// The reason why the operation is invalid. + /// The exception message is composed as "<operation>: <reason>". public InvalidDomainOperationException(string operation, string reason) : base($"{operation}: {reason}") { Reason = reason; } } -} +} \ No newline at end of file diff --git a/Core/Domain/ValueObjects/Email.cs b/Core/Domain/ValueObjects/Email.cs index 2873723..0421ea6 100644 --- a/Core/Domain/ValueObjects/Email.cs +++ b/Core/Domain/ValueObjects/Email.cs @@ -102,7 +102,10 @@ public class Email : ValueObject /// Private constructor ensures that emails can only be created /// through the Create factory method, which performs validation. /// This is a key pattern in Domain-Driven Design. - /// + /// + /// Initializes a new instance with a validated, normalized email value. + /// + /// The validated, lowercase email address to store as the value object. private Email(string value) { Value = value; @@ -141,7 +144,12 @@ private Email(string value) /// Uses System.Net.Mail.MailAddress for validation, which is /// comprehensive but not the fastest option. For high-throughput /// scenarios, consider regex-based validation. - /// + /// + /// Creates an Email value object from the provided string after validating and normalizing it. + /// + /// The email address to validate and convert; leading and trailing whitespace are ignored. + /// An Email instance containing the validated email stored in lowercase. + /// Thrown if the input is null, empty, or whitespace; if the trimmed address exceeds 320 characters; or if the address has an invalid format. public static Email Create(string email) { // Validation 1: Check for null or empty @@ -201,7 +209,11 @@ public static Email Create(string email) /// ``` /// /// Note: Regex is faster but less comprehensive than MailAddress. - /// + /// + /// Determines whether the provided string is a syntactically valid email address and contains only the address portion (no display name). + /// + /// The candidate email string to validate. + /// `true` if the string is a valid email address and matches the parsed address exactly, `false` otherwise. private static bool IsValidEmail(string email) { try @@ -228,7 +240,10 @@ private static bool IsValidEmail(string email) /// The local part of the email. /// /// For "user.name@example.com", returns "user.name" - /// + /// + /// Gets the local part (the substring before the '@') of the email value. + /// + /// The substring before the '@', or the entire email value if the address has no local part (missing or starting with '@'). public string GetLocalPart() { var atIndex = Value.IndexOf('@'); @@ -241,7 +256,10 @@ public string GetLocalPart() /// The domain part of the email. /// /// For "user@example.com", returns "example.com" - /// + /// + /// Gets the domain portion of the email (the substring after the '@'). + /// + /// The domain part of the email, or an empty string if the email has no '@' or no domain portion. public string GetDomain() { var atIndex = Value.IndexOf('@'); @@ -262,7 +280,11 @@ public string GetDomain() /// email.IsFromDomain("EXAMPLE.COM"); // True (case-insensitive) /// email.IsFromDomain("subdomain.example.com"); // False /// ``` - /// + /// + /// Determines whether the email's domain equals the specified domain (case-insensitive). + /// + /// The domain to compare against (leading/trailing whitespace is ignored). + /// `true` if the email's domain matches `domain` (case-insensitive), `false` otherwise. public bool IsFromDomain(string domain) { if (string.IsNullOrWhiteSpace(domain)) @@ -294,7 +316,15 @@ public bool IsFromDomain(string domain) /// "ab@example.com" ? "ab@example.com" /// "longusername@example.com" ? "lo********me@example.com" /// ``` + /// + /// Produces a privacy-preserving display form of the email by masking part of the local part. + /// + /// + /// If the local part has 3 or fewer characters, the original email value is returned unchanged. + /// For longer local parts, the method keeps the first two and last two characters, replaces the middle with asterisks (at least two), and preserves the domain. + /// Examples: "ab@example.com" -> "ab@example.com", "username@example.com" -> "us****me@example.com". /// + /// The masked email string suitable for display. public string ToMaskedString() { var localPart = GetLocalPart(); @@ -320,7 +350,10 @@ public string ToMaskedString() /// /// Returns the email address string. /// - /// The email address. + /// + /// Gets the underlying normalized email string. + /// + /// The normalized email address stored by this instance. public override string ToString() { return Value; @@ -333,7 +366,10 @@ public override string ToString() /// /// Emails are compared by their normalized value (lowercase). /// "USER@EXAMPLE.COM" == "user@example.com" returns true. - /// + /// + /// Provides the sequence of components that define this value object's equality. + /// + /// An that yields the email's normalized value. protected override IEnumerable GetEqualityComponents() { yield return Value; @@ -348,4 +384,4 @@ public static implicit operator string(Email email) return email.Value; } } -} +} \ No newline at end of file diff --git a/Core/Domain/ValueObjects/Role.cs b/Core/Domain/ValueObjects/Role.cs index 1840050..b59e676 100644 --- a/Core/Domain/ValueObjects/Role.cs +++ b/Core/Domain/ValueObjects/Role.cs @@ -72,7 +72,10 @@ public class Role : ValueObject /// /// Private constructor to enforce factory method usage. /// - /// The role name. + /// + /// Initializes a Role instance with the specified canonical role name. + /// + /// The canonical role name (for example: "User", "Admin", "SuperAdmin"). private Role(string name) { Name = name; @@ -161,7 +164,12 @@ private Role(string name) /// - Consistent error messages /// - Easy to extend with new roles /// - Thread-safe for predefined roles - /// + /// + /// Creates a Role from a string representation, validating and mapping it to a predefined Role instance. + /// + /// The role name to parse; accepts "User", "Admin", "SuperAdmin" (also "Super-Admin"), matched case-insensitively. + /// The corresponding predefined instance. + /// Thrown when is null, empty, whitespace, or does not match a valid predefined role. public static Role Create(string roleName) { if (string.IsNullOrWhiteSpace(roleName)) @@ -205,7 +213,10 @@ public static Role Create(string roleName) /// // Proceed with deletion /// } /// ``` - /// + /// + /// Determines whether the role has administrator privileges. + /// + /// `true` if the role is `Admin` or `SuperAdmin`, `false` otherwise. public bool IsAdmin() { return this == Admin || this == SuperAdmin; @@ -233,7 +244,10 @@ public bool IsAdmin() /// // Proceed with changes /// } /// ``` - /// + /// + /// Determines whether this role is the SuperAdmin role. + /// + /// `true` if the role is SuperAdmin, `false` otherwise. public bool IsSuperAdmin() { return this == SuperAdmin; @@ -254,7 +268,10 @@ public bool IsSuperAdmin() /// await _rateLimiter.CheckLimitAsync(currentUser.Id); /// } /// ``` - /// + /// + /// Determines whether the role represents the standard User role. + /// + /// true if this role is User; false otherwise. public bool IsUser() { return this == User; @@ -281,7 +298,11 @@ public bool IsUser() /// return currentUser.Roles.Any(r => r.HasPermission(Role.Admin)); /// } /// ``` - /// + /// + /// Determines whether the current role satisfies a required role according to the role hierarchy. + /// + /// The minimum role required for the operation; if null, the requirement is not satisfied. + /// `true` if the current role meets or exceeds the required role in the hierarchy (SuperAdmin > Admin > User), `false` otherwise. public bool HasPermission(Role requiredRole) { if (requiredRole == null) @@ -313,7 +334,10 @@ public bool HasPermission(Role requiredRole) /// - Role.User ? "User" /// - Role.Admin ? "Administrator" /// - Role.SuperAdmin ? "Super Administrator" - /// + /// + /// Gets a user-friendly display name for the role. + /// + /// The display name: "User" for User, "Administrator" for Admin, "Super Administrator" for SuperAdmin; otherwise the raw role name. public string GetDisplayName() { return Name switch @@ -328,7 +352,10 @@ public string GetDisplayName() /// /// Returns the role name. /// - /// The role name string. + /// + /// Gets the role's name for display or logging. + /// + /// The underlying role name. public override string ToString() { return Name; @@ -341,7 +368,10 @@ public override string ToString() /// /// Roles are compared by their name. /// Role.Create("Admin") == Role.Admin returns true. - /// + /// + /// Provides the components used to determine value equality for this Role. + /// + /// An enumerable of equality-significant components; yields the role's Name. protected override IEnumerable GetEqualityComponents() { yield return Name; @@ -376,7 +406,10 @@ public static implicit operator string(Role role) /// }); /// } /// ``` - /// + /// + /// Enumerates all predefined Role instances. + /// + /// An enumerable containing the predefined Role instances: User, Admin, and SuperAdmin. public static IEnumerable GetAllRoles() { yield return User; @@ -384,4 +417,4 @@ public static IEnumerable GetAllRoles() yield return SuperAdmin; } } -} +} \ No newline at end of file diff --git a/Core/Domain/ValueObjects/ValueObject.cs b/Core/Domain/ValueObjects/ValueObject.cs index 58a0fd5..6d00458 100644 --- a/Core/Domain/ValueObjects/ValueObject.cs +++ b/Core/Domain/ValueObjects/ValueObject.cs @@ -85,7 +85,12 @@ public abstract class ValueObject /// /// Order matters! Components should be yielded in a consistent order /// for proper equality comparison. - /// + /// +/// Provides the sequence of values that define this value object's identity for equality comparison. +/// +/// +/// An ordered sequence of component values used to determine equality; elements may be null and the order of items is significant. +/// protected abstract IEnumerable GetEqualityComponents(); /// @@ -96,7 +101,11 @@ public abstract class ValueObject /// /// Equality is determined by comparing all equality components. /// Two value objects are equal if all their components are equal. - /// + /// + /// Determines whether this value object is equal to another object of the same runtime type by comparing their equality components. + /// + /// The object to compare with the current value object. + /// true if is a of the same runtime type and all equality components are equal; false otherwise. public override bool Equals(object? obj) { if (obj == null || obj.GetType() != GetType()) @@ -121,7 +130,10 @@ public override bool Equals(object? obj) /// - Dictionary/HashSet usage /// - Entity Framework change tracking /// - Caching mechanisms - /// + /// + /// Produces a hash code that represents the value object's equality-defining components. + /// + /// An integer hash code derived from the value object's equality components. public override int GetHashCode() { return GetEqualityComponents() @@ -158,10 +170,16 @@ public override int GetHashCode() /// /// Since value objects are immutable, this performs a memberwise clone. /// Useful when you need a new instance for modifications in derived classes. + /// + /// Creates a shallow copy of the current value object instance. + /// + /// + /// The copy is a new instance of the same runtime type containing the same component values; reference-type components are copied by reference. /// + /// A new ValueObject instance with the same component values as the original. protected ValueObject GetCopy() { return (ValueObject)MemberwiseClone(); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Caching/CacheService.cs b/Infrastructure/Caching/CacheService.cs index 96be9bd..4af5840 100644 --- a/Infrastructure/Caching/CacheService.cs +++ b/Infrastructure/Caching/CacheService.cs @@ -22,6 +22,9 @@ public class CacheService : ICacheService private readonly IDistributedCache _cache; private readonly ILogger _logger; + /// + /// Initializes a new instance of with the specified distributed cache and logger. + /// public CacheService(IDistributedCache cache, ILogger logger) { _cache = cache; @@ -30,7 +33,12 @@ public CacheService(IDistributedCache cache, ILogger logger) /// /// Retrieves a cached value by its key. + /// + /// Retrieve a cached value by key and deserialize it to the specified type. /// + /// The cache key to read. + /// Token to cancel the cache retrieval operation. + /// The cached value deserialized to , or `default(T)` if the key is not present or an error occurs. public async Task GetAsync(string key, CancellationToken cancellationToken = default) { try @@ -55,7 +63,14 @@ public CacheService(IDistributedCache cache, ILogger logger) /// /// Stores a value in the cache with an optional expiration time. + /// + /// Stores a value in the distributed cache under the specified key using JSON serialization. /// + /// Cache key to store the value under. + /// Value to serialize and store. + /// Optional absolute expiration relative to now; if null defaults to 5 minutes. + /// Token to cancel the cache operation. + /// Exceptions during serialization or cache operations are caught and logged; the method does not propagate them. public async Task SetAsync( string key, T value, @@ -83,7 +98,11 @@ public async Task SetAsync( /// /// Removes a cached value by its key. + /// + /// Removes the cache entry identified by the specified key if it exists. /// + /// Cache key of the entry to remove. + /// Token to cancel the removal operation. public async Task RemoveAsync(string key, CancellationToken cancellationToken = default) { try @@ -99,7 +118,11 @@ public async Task RemoveAsync(string key, CancellationToken cancellationToken = /// /// Checks if a key exists in the cache. + /// + /// Determines whether a cache entry exists for the specified key. /// + /// The cache key to check. + /// `true` if a non-empty value is stored for the key, `false` otherwise. public async Task ExistsAsync(string key, CancellationToken cancellationToken = default) { try @@ -117,4 +140,4 @@ public async Task ExistsAsync(string key, CancellationToken cancellationTo } } } -} +} \ No newline at end of file diff --git a/Infrastructure/Caching/SampleCache.cs b/Infrastructure/Caching/SampleCache.cs index e18f4b3..9390585 100644 --- a/Infrastructure/Caching/SampleCache.cs +++ b/Infrastructure/Caching/SampleCache.cs @@ -11,7 +11,16 @@ namespace SecureCleanApiWaf.Infrastructure.Caching public class SampleCache { private readonly IMemoryCache _cache; - public SampleCache(IMemoryCache cache) => _cache = cache; + /// +/// Initializes a new instance of that uses the provided memory cache. +/// +/// The instance used to store and retrieve cached values. +public SampleCache(IMemoryCache cache) => _cache = cache; + /// + /// Retrieves the cached string for the specified key; if the key is not present, stores and returns the string "Cached value". + /// + /// The cache entry key. + /// The cached string associated with ; if the key was not present, the newly stored value "Cached value". public string GetOrSet(string key) { if (!_cache.TryGetValue(key, out string value)) @@ -22,4 +31,4 @@ public string GetOrSet(string key) return value; } } -} +} \ No newline at end of file diff --git a/Infrastructure/Data/ApplicationDbContext.cs b/Infrastructure/Data/ApplicationDbContext.cs index 10f3dc1..6cfd6ed 100644 --- a/Infrastructure/Data/ApplicationDbContext.cs +++ b/Infrastructure/Data/ApplicationDbContext.cs @@ -47,7 +47,10 @@ public class ApplicationDbContext : DbContext /// /// Initializes a new instance of the ApplicationDbContext. /// - /// Configuration options for the context. + /// + /// Initializes a new instance of ApplicationDbContext configured with the provided options. + /// + /// The options used to configure the DbContext (e.g., provider and connection settings). public ApplicationDbContext(DbContextOptions options) : base(options) { @@ -98,6 +101,13 @@ public ApplicationDbContext(DbContextOptions options) /// - Easier to maintain and test /// - Better organization for large models /// - Reusable configurations + /// + /// Registers entity configurations from this assembly and configures global soft-delete query filters for ApiDataItem, User, and Token. + /// + /// The used to construct the EF Core model. + /// + /// Applies all IEntityTypeConfiguration implementations found in the context assembly and adds query filters that exclude entities with IsDeleted == true. + /// Use IgnoreQueryFilters() on a query to include soft-deleted entities. /// protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -138,7 +148,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) /// - Inconsistent timestamp handling /// - Manual timestamp updates in repositories /// - Timezone issues (always uses UTC) - /// + /// + /// Persists changes to the database while automatically setting audit timestamps on added and modified BaseEntity instances. + /// + /// A token to observe while waiting for the save operation to complete. + /// The number of state entries written to the underlying database. + /// Sets `CreatedAt` to the current UTC time for newly added BaseEntity instances and `UpdatedAt` to the current UTC time for modified BaseEntity instances. public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { // Get all tracked entities that inherit from BaseEntity @@ -170,7 +185,13 @@ public override async Task SaveChangesAsync(CancellationToken cancellationT /// /// Overridden to maintain consistency with async version. /// However, prefer using SaveChangesAsync for better scalability. + /// + /// Persists changes to the database while applying audit timestamps to tracked BaseEntity instances. + /// + /// + /// Sets `CreatedAt` to the current UTC time for added entities and `UpdatedAt` to the current UTC time for modified entities before saving. /// + /// The number of state entries written to the database. public override int SaveChanges() { // Get all tracked entities that inherit from BaseEntity @@ -195,4 +216,4 @@ public override int SaveChanges() return base.SaveChanges(); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Data/Configurations/ApiDataItemConfiguration.cs b/Infrastructure/Data/Configurations/ApiDataItemConfiguration.cs index d210f16..95de7f0 100644 --- a/Infrastructure/Data/Configurations/ApiDataItemConfiguration.cs +++ b/Infrastructure/Data/Configurations/ApiDataItemConfiguration.cs @@ -36,7 +36,10 @@ public class ApiDataItemConfiguration : IEntityTypeConfiguration /// /// Configures the ApiDataItem entity. /// - /// Entity type builder for ApiDataItem. + /// + /// Configures the EF Core mapping for the ApiDataItem entity, defining its table name, column types and constraints, indexes, JSON metadata conversion, soft-delete and audit properties, and optimistic concurrency behavior. + /// + /// EntityTypeBuilder for ApiDataItem used to configure table mapping, columns, indexes, conversions, and concurrency. public void Configure(EntityTypeBuilder builder) { // ===== TABLE CONFIGURATION ===== @@ -177,4 +180,4 @@ public void Configure(EntityTypeBuilder builder) .HasColumnName("RowVersion"); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Data/DatabaseSettings.cs b/Infrastructure/Data/DatabaseSettings.cs index 393b721..e0342ce 100644 --- a/Infrastructure/Data/DatabaseSettings.cs +++ b/Infrastructure/Data/DatabaseSettings.cs @@ -215,7 +215,10 @@ public class DatabaseSettings /// - Valid retry configuration /// /// Call this during startup to fail fast if misconfigured. - /// + /// + /// Validates the database configuration values against required constraints. + /// + /// `true` if the ConnectionString is not empty, CommandTimeout is greater than 0, MaxRetryCount is between 0 and 10 inclusive, and MaxRetryDelay is greater than 0; `false` otherwise. public bool IsValid() { if (string.IsNullOrWhiteSpace(ConnectionString)) @@ -243,7 +246,10 @@ public bool IsValid() /// Example: /// Input: "Server=localhost;Database=MyDb;User=sa;Password=secret123;" /// Output: "Server=localhost;Database=MyDb;User=sa;Password=***;" - /// + /// + /// Produces a connection string with embedded passwords redacted for safe logging. + /// + /// An empty string if is null or whitespace; otherwise the connection string with password values replaced with `***`. public string GetSanitizedConnectionString() { if (string.IsNullOrWhiteSpace(ConnectionString)) @@ -266,4 +272,4 @@ public string GetSanitizedConnectionString() return sanitized; } } -} +} \ No newline at end of file diff --git a/Infrastructure/Handlers/ApiKeyHandler.cs b/Infrastructure/Handlers/ApiKeyHandler.cs index a66908f..e523b72 100644 --- a/Infrastructure/Handlers/ApiKeyHandler.cs +++ b/Infrastructure/Handlers/ApiKeyHandler.cs @@ -22,12 +22,21 @@ public class ApiKeyHandler : DelegatingHandler private readonly IConfiguration _configuration; private readonly ILogger _logger; + /// + /// Initializes a new instance of with the provided configuration and logger. + /// public ApiKeyHandler(IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; } + /// + /// Adds an API key and standard headers to the outgoing HTTP request, sends it through the handler pipeline, and logs timing and response details. + /// + /// The outgoing HTTP request to modify (headers may be added) and send. + /// Cancellation token to cancel the send operation. + /// The HTTP response returned by the downstream handler. protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) @@ -96,4 +105,4 @@ protected override async Task SendAsync( } } } -} +} \ No newline at end of file diff --git a/Infrastructure/Middleware/JwtBlacklistValidationMiddleware.cs b/Infrastructure/Middleware/JwtBlacklistValidationMiddleware.cs index 9583dbb..259460b 100644 --- a/Infrastructure/Middleware/JwtBlacklistValidationMiddleware.cs +++ b/Infrastructure/Middleware/JwtBlacklistValidationMiddleware.cs @@ -47,7 +47,12 @@ public class JwtBlacklistValidationMiddleware /// /// The next middleware in the pipeline /// MediatR instance for CQRS queries - /// Logger for security events and debugging + /// + /// Middleware that validates JWT tokens against a blacklist after authentication and before authorization. + /// + /// The next middleware delegate in the pipeline. + /// MediatR mediator used to dispatch queries to check token blacklist status. + /// Thrown if , , or is null. public JwtBlacklistValidationMiddleware( RequestDelegate next, IMediator mediator, @@ -61,7 +66,11 @@ public JwtBlacklistValidationMiddleware( /// /// Processes HTTP requests to validate JWT tokens against the blacklist using CQRS. /// - /// The HTTP context for the current request + /// + /// Validates a Bearer JWT from the current HTTP request against the token blacklist and either rejects blacklisted tokens or continues the pipeline. + /// + /// The HTTP context for the current request; used to read the Authorization header and to write a 401 response for blacklisted tokens. + /// A task that completes when the middleware has finished processing the request. public async Task InvokeAsync(HttpContext context) { try @@ -137,7 +146,11 @@ public async Task InvokeAsync(HttpContext context) /// Extracts JWT token from the Authorization header. /// /// HTTP context containing the request - /// JWT token string or null if not found + /// + /// Extracts the Bearer JWT from the request's Authorization header if present and appears to be a well-formed JWT. + /// + /// The current HTTP context whose request headers are inspected for a Bearer token. + /// The Bearer JWT string when present and consisting of three dot-separated parts; `null` otherwise. private string? ExtractTokenFromRequest(HttpContext context) { try @@ -190,7 +203,12 @@ public async Task InvokeAsync(HttpContext context) /// /// HTTP context for the current request /// The blacklisted token - /// Detailed blacklist status from CQRS query + /// + /// Produce a 401 Unauthorized response for a request carrying a blacklisted JWT, emit a security log, and write an enhanced JSON error payload. + /// + /// The current HTTP context for the request/response. + /// The raw JWT string that was identified as blacklisted (used for logging/context). + /// Detailed blacklist status returned by the CQRS query (used to populate logs and the error payload). private async Task HandleBlacklistedToken(HttpContext context, string token, TokenBlacklistStatusDto blacklistStatus) { try @@ -280,10 +298,17 @@ public static class JwtBlacklistValidationMiddlewareExtensions /// 2. Use CQRS queries to check tokens against the blacklist service /// 3. Return 401 for blacklisted tokens with detailed information /// 4. Allow valid tokens to continue with automatic caching benefits + /// + /// Registers the JWT blacklist validation middleware into the ASP.NET Core request pipeline. + /// + /// The application builder to add the middleware to. + /// The same instance so additional middleware can be chained. + /// + /// The middleware should be placed after authentication and before authorization (for example, after UseAuthentication() and before UseAuthorization()) so that authenticated tokens are checked against the blacklist prior to authorization decisions. /// public static IApplicationBuilder UseJwtBlacklistValidation(this IApplicationBuilder builder) { return builder.UseMiddleware(); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Repositories/ApiDataItemRepository.cs b/Infrastructure/Repositories/ApiDataItemRepository.cs index efe4b35..e258028 100644 --- a/Infrastructure/Repositories/ApiDataItemRepository.cs +++ b/Infrastructure/Repositories/ApiDataItemRepository.cs @@ -40,7 +40,10 @@ public class ApiDataItemRepository : IApiDataItemRepository /// /// Initializes a new instance of the ApiDataItemRepository. /// - /// EF Core database context. + /// + /// Initializes a new instance of using the provided EF Core . + /// + /// The application's EF Core database context used for data access; must not be null. public ApiDataItemRepository(ApplicationDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); @@ -77,7 +80,10 @@ public async Task> GetActiveItemsAsync(CancellationTo .ToListAsync(cancellationToken); } - /// + /// + /// Retrieves all ApiDataItem entities, including soft-deleted items, ordered by creation time descending. + /// + /// A read-only list of all ApiDataItem entities ordered by CreatedAt descending. public async Task> GetAllItemsAsync(CancellationToken cancellationToken = default) { // Note: This ignores the global query filter to include deleted items @@ -99,7 +105,11 @@ public async Task> GetItemsByStatusAsync(DataStatus s .ToListAsync(cancellationToken); } - /// + /// + /// Retrieves items whose LastSyncedAt is older than the specified maximum age, prioritizing the oldest first. + /// + /// Maximum allowed age since an item's LastSyncedAt; items older than this value are included. + /// A read-only list of ApiDataItem instances with Status not equal to Deleted and LastSyncedAt earlier than (UtcNow - maxAge), ordered from oldest to newest LastSyncedAt. public async Task> GetItemsNeedingRefreshAsync(TimeSpan maxAge, CancellationToken cancellationToken = default) { var cutoffDate = DateTime.UtcNow - maxAge; @@ -145,7 +155,16 @@ public async Task> SearchByNameAsync(string searchTer .ToListAsync(cancellationToken); } - /// + /// + /// Retrieves ApiDataItem entities that contain the specified metadata key, optionally filtered to those whose metadata value equals the provided value. + /// + /// The metadata key to match. If null or whitespace, an empty list is returned. + /// Optional metadata value to match; when null, any item that has the key is included. + /// Token to observe while waiting for the task to complete. + /// A list of items that have the specified metadata key and, if is provided, whose metadata value equals it. + /// + /// Filtering is performed in memory after loading all items from the database; this may be inefficient for large datasets. + /// public async Task> GetItemsByMetadataAsync( string metadataKey, object? metadataValue = null, @@ -182,7 +201,11 @@ public async Task ExistsAsync(string externalId, CancellationToken cancell .AnyAsync(x => x.ExternalId == externalId && x.Status != DataStatus.Deleted, cancellationToken); } - /// + /// + /// Registers an with the repository for insertion on the next unit-of-work save. + /// + /// The to add; must not be null. + /// Thrown when is null. public async Task AddAsync(ApiDataItem item, CancellationToken cancellationToken = default) { if (item == null) @@ -219,7 +242,13 @@ public async Task UpdateAsync(ApiDataItem item, CancellationToken cancellationTo await Task.CompletedTask; // Maintain async signature } - /// + /// + /// Updates a collection of ApiDataItem entities in the context and persists the changes. + /// + /// The items to update; must not be null or empty. + /// Token to cancel the operation. + /// The number of state entries written to the database. + /// Thrown when is null. public async Task UpdateRangeAsync(IEnumerable items, CancellationToken cancellationToken = default) { if (items == null) @@ -234,7 +263,12 @@ public async Task UpdateRangeAsync(IEnumerable items, Cancella return await SaveChangesAsync(cancellationToken); } - /// + /// + /// Marks the given ApiDataItem as deleted in the DbContext without persisting the change. + /// + /// The ApiDataItem to mark as deleted; expected to have been marked deleted prior to calling. + /// Thrown when is null. + /// Does not call SaveChangesAsync — the caller is responsible for persisting the change. public async Task DeleteAsync(ApiDataItem item, CancellationToken cancellationToken = default) { if (item == null) @@ -246,7 +280,11 @@ public async Task DeleteAsync(ApiDataItem item, CancellationToken cancellationTo await Task.CompletedTask; // Maintain async signature } - /// + /// + /// Permanently removes items that were soft-deleted before the specified cutoff date. + /// + /// Items with a non-null DeletedAt earlier than this UTC timestamp will be hard-deleted. + /// The number of items that were removed from the database. public async Task PermanentlyDeleteOldItemsAsync(DateTime olderThan, CancellationToken cancellationToken = default) { // Find items to permanently delete @@ -263,7 +301,12 @@ public async Task PermanentlyDeleteOldItemsAsync(DateTime olderThan, Cancel return await SaveChangesAsync(cancellationToken); } - /// + /// + /// Marks all active ApiDataItem entities that share the given source URL as stale. + /// + /// The source URL whose active items should be marked stale; if null or whitespace no items are modified. + /// Cancellation token to cancel the operation. + /// The number of entries persisted to the database (0 if no items were modified). public async Task MarkSourceAsStaleAsync(string sourceUrl, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(sourceUrl)) @@ -286,7 +329,18 @@ public async Task MarkSourceAsStaleAsync(string sourceUrl, CancellationToke return await SaveChangesAsync(cancellationToken); } - /// + /// + /// Aggregates diagnostic statistics for ApiDataItem entities. + /// + /// Token to cancel the database queries. + /// + /// An ApiDataStatisticsDto containing: + /// - total counts of active, stale, and deleted items (deleted count ignores global query filters), + /// - average, oldest, and newest ages computed from LastSyncedAt, + /// - count of distinct source URLs, + /// - counts of items synced or marked stale in the last 24 hours, + /// - the UTC timestamp when the statistics were calculated. + /// public async Task GetStatisticsAsync(CancellationToken cancellationToken = default) { var now = DateTime.UtcNow; @@ -360,4 +414,4 @@ public async Task SaveChangesAsync(CancellationToken cancellationToken = de return await _context.SaveChangesAsync(cancellationToken); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Repositories/TokenRepository.cs b/Infrastructure/Repositories/TokenRepository.cs index 1e20b44..0a0ddd0 100644 --- a/Infrastructure/Repositories/TokenRepository.cs +++ b/Infrastructure/Repositories/TokenRepository.cs @@ -30,13 +30,21 @@ public class TokenRepository : ITokenRepository /// /// Initializes a new instance of the TokenRepository. /// - /// EF Core database context. + /// + /// Initializes a new instance of backed by the supplied EF Core context. + /// + /// The EF Core used for token data access. + /// Thrown when is null. public TokenRepository(ApplicationDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } - /// + /// + /// Retrieve the token with the specified identifier. + /// + /// The unique identifier of the token to retrieve. + /// The token with the specified Id, or `null` if no matching token is found. public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { return await _context.Tokens @@ -44,7 +52,11 @@ public TokenRepository(ApplicationDbContext context) .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); } - /// + /// + /// Retrieve the token that matches the specified token identifier. + /// + /// The token identifier to search for. If null, empty, or whitespace, no lookup is performed. + /// The matching if found; otherwise null. public async Task GetByTokenIdAsync(string tokenId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(tokenId)) @@ -90,7 +102,10 @@ public async Task> GetTokensByUserAndTypeAsync(Guid userId, .ToListAsync(cancellationToken); } - /// + /// + /// Retrieves all tokens marked as revoked, ordered by most recent revocation first. + /// + /// A read-only list of revoked tokens ordered by `RevokedAt` descending. public async Task> GetRevokedTokensAsync(CancellationToken cancellationToken = default) { return await _context.Tokens @@ -145,7 +160,12 @@ public async Task IsTokenBlacklistedAsync(string tokenId, CancellationToke cancellationToken); } - /// + /// + /// Adds the specified Token entity to the EF Core context so it will be persisted on the next save. + /// + /// The Token entity to add. + /// Token to observe while waiting for the add operation to complete. + /// Thrown when is null. public async Task AddAsync(Token token, CancellationToken cancellationToken = default) { if (token == null) @@ -164,7 +184,13 @@ public async Task UpdateAsync(Token token, CancellationToken cancellationToken = await Task.CompletedTask; } - /// + /// + /// Marks the specified token for deletion from the database context. + /// + /// The token entity to remove; cannot be null. + /// + /// The token is removed from the DbContext. Changes are not persisted until is called. + /// public async Task DeleteAsync(Token token, CancellationToken cancellationToken = default) { if (token == null) @@ -210,7 +236,10 @@ public async Task RevokeAllUserTokensAsync(Guid userId, string reason, Canc return await SaveChangesAsync(cancellationToken); } - /// + /// + /// Computes aggregate counts for tokens (active, revoked, expired) and by type, using the current UTC time. + /// + /// A object containing counts for active, revoked, expired, access, and refresh tokens, and the UTC timestamp when the values were calculated. public async Task GetTokenStatisticsAsync(CancellationToken cancellationToken = default) { var now = DateTime.UtcNow; @@ -252,4 +281,4 @@ public async Task SaveChangesAsync(CancellationToken cancellationToken = de return await _context.SaveChangesAsync(cancellationToken); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Repositories/UserRepository.cs b/Infrastructure/Repositories/UserRepository.cs index 1ec960d..bd48b3c 100644 --- a/Infrastructure/Repositories/UserRepository.cs +++ b/Infrastructure/Repositories/UserRepository.cs @@ -32,7 +32,11 @@ public class UserRepository : IUserRepository /// /// Initializes a new instance of the UserRepository. /// - /// EF Core database context. + /// + /// Initializes a new instance of using the provided application database context. + /// + /// The application's EF Core used for data access. + /// Thrown when is null. public UserRepository(ApplicationDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); @@ -68,7 +72,12 @@ public UserRepository(ApplicationDbContext context) .FirstOrDefaultAsync(x => x.Email == email, cancellationToken); } - /// + /// + /// Retrieves users assigned to the specified role, ordered by username. + /// + /// Role to filter users by. If null, the method returns an empty list. + /// Token to cancel the asynchronous operation. + /// A read-only list of users that have the specified role, ordered by Username; empty if no users match or if is null. public async Task> GetUsersByRoleAsync(Role role, CancellationToken cancellationToken = default) { if (role == null) @@ -83,7 +92,12 @@ public async Task> GetUsersByRoleAsync(Role role, Cancellati .ToListAsync(cancellationToken); } - /// + /// + /// Adds a new User entity to the DbContext change tracker so it will be inserted on the next save. + /// + /// The User entity to add; must not be null. + /// Token to observe while waiting for the add operation to complete. + /// Thrown when is null. public async Task AddAsync(User user, CancellationToken cancellationToken = default) { if (user == null) @@ -102,7 +116,12 @@ public async Task UpdateAsync(User user, CancellationToken cancellationToken = d await Task.CompletedTask; } - /// + /// + /// Marks the provided user entity as deleted by updating it in the DbContext (soft delete). + /// + /// The user entity to mark deleted; must not be null and should already have deletion flags set. + /// The cancellation token to observe. + /// Thrown when is null. public async Task DeleteAsync(User user, CancellationToken cancellationToken = default) { if (user == null) @@ -135,7 +154,11 @@ public async Task EmailExistsAsync(Email email, CancellationToken cancella .AnyAsync(x => x.Email == email, cancellationToken); } - /// + /// + /// Retrieves users whose failed login attempts meet or exceed the specified threshold, ordered by attempts descending. + /// + /// Minimum number of failed login attempts a user must have to be included. + /// A read-only list of users with FailedLoginAttempts greater than or equal to , ordered from highest to lowest attempts. public async Task> GetUsersWithFailedLoginAttemptsAsync(int threshold, CancellationToken cancellationToken = default) { return await _context.Users @@ -151,4 +174,4 @@ public async Task SaveChangesAsync(CancellationToken cancellationToken = de return await _context.SaveChangesAsync(cancellationToken); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Security/JwtTokenGenerator.cs b/Infrastructure/Security/JwtTokenGenerator.cs index bcbf8db..55dc9d6 100644 --- a/Infrastructure/Security/JwtTokenGenerator.cs +++ b/Infrastructure/Security/JwtTokenGenerator.cs @@ -17,6 +17,9 @@ public class JwtTokenGenerator { private readonly IConfiguration _configuration; + /// + /// Initializes a new instance that uses the provided application configuration to read JWT settings for token generation. + /// public JwtTokenGenerator(IConfiguration configuration) { _configuration = configuration; @@ -39,6 +42,13 @@ public JwtTokenGenerator(IConfiguration configuration) /// empty, no role claims are included. /// A string representing the serialized JWT. The token includes user identity and role claims, and is signed /// using the configured secret key. + /// + /// Generate a signed JSON Web Token containing user identity and optional role claims. + /// + /// The unique identifier for the user, stored as the `sub` claim. + /// The user's username, stored as the `unique_name` claim. + /// Optional array of role names to include as separate role claims; pass null or an empty array to omit role claims. + /// Compact serialized JWT string. /// Thrown if the JWT secret key is not configured in the application settings. public string GenerateToken(string userId, string username, string[] roles = null) { @@ -142,7 +152,11 @@ public string GenerateToken(string userId, string username, string[] roles = nul /// - Single role: "User" /// /// Usage: var token = tokenGenerator.GenerateUserToken("john.doe"); - /// + /// + /// Generates a JWT for a standard user and includes the "User" role. + /// + /// The username to embed in the token; defaults to "testuser". + /// The serialized JWT string containing the user's identity and roles. public string GenerateUserToken(string username = "testuser") { // Generate a user token with basic "User" role @@ -166,7 +180,11 @@ public string GenerateUserToken(string username = "testuser") /// while the "Admin" role grants access to administrative endpoints. /// /// Usage: var token = tokenGenerator.GenerateAdminToken("admin.user"); - /// + /// + /// Generates a JWT for an administrative user. + /// + /// Username to include in the token; defaults to "admin". + /// The serialized JWT containing both "User" and "Admin" role claims. public string GenerateAdminToken(string username = "admin") { // Generate an admin token with both "User" and "Admin" roles @@ -174,4 +192,4 @@ public string GenerateAdminToken(string username = "admin") return GenerateToken(Guid.NewGuid().ToString(), username, new[] { "User", "Admin" }); } } -} +} \ No newline at end of file diff --git a/Infrastructure/Services/ApiIntegrationService.cs b/Infrastructure/Services/ApiIntegrationService.cs index aef64ef..d7bb318 100644 --- a/Infrastructure/Services/ApiIntegrationService.cs +++ b/Infrastructure/Services/ApiIntegrationService.cs @@ -28,6 +28,9 @@ public class ApiIntegrationService : IApiIntegrationService private readonly ILogger _logger; private readonly ApiDataMapper _mapper; + /// + /// Initializes a new instance of with its required dependencies. + /// public ApiIntegrationService( IHttpClientFactory httpClientFactory, ILogger logger, @@ -43,7 +46,11 @@ public ApiIntegrationService( /// Logs request, response, exceptions, and latency. /// /// The third-party API endpoint. Relative path - /// Result containing API response or error details. + /// + /// Fetches JSON from the specified API endpoint and deserializes it to the requested type `T`. + /// + /// The API endpoint to request. Prefer a path relative to the named client's base address; if a full URL is provided the client's base address will be ignored. + /// Result containing the deserialized response as `T` on success, or a failed Result with an error message on failure. public async Task> GetAllDataAsync(string apiUrl) { var startTime = DateTime.UtcNow; @@ -83,7 +90,12 @@ public async Task> GetAllDataAsync(string apiUrl) /// /// The third-party API endpoint. Relative path /// The identifier for the data to retrieve - /// Result containing API response or error details. + /// + /// Fetches a single resource from the specified API by its identifier and deserializes the JSON response to an instance of . + /// + /// The base URL of the API endpoint. + /// The identifier of the resource to fetch; appended to to form the request URL. + /// Result containing the deserialized `` on success, or a failed Result with an error message on failure. public async Task> GetDataByIdAsync(string apiUrl, string id) { var startTime = DateTime.UtcNow; @@ -113,7 +125,11 @@ public async Task> GetDataByIdAsync(string apiUrl, string id) } } - /// + /// + /// Fetches data from the specified API endpoint, maps the response to domain ApiDataItem objects, and returns the mapped list wrapped in a Result. + /// + /// The API endpoint URL to fetch data from. + /// A Result containing the list of mapped objects on success; on failure the Result contains an error message. public async Task>> GetApiDataItemsAsync(string apiUrl) { try @@ -149,7 +165,12 @@ public async Task>> GetApiDataItemsAsync(string apiUrl) } } - /// + /// + /// Fetches a single API resource identified by from and maps it to an . + /// + /// Base URL of the third-party API endpoint. + /// Identifier of the resource to fetch. + /// A containing the mapped on success, or a failed Result with an error message on failure. public async Task> GetApiDataItemByIdAsync(string apiUrl, string id) { try @@ -184,7 +205,11 @@ public async Task> GetApiDataItemByIdAsync(string apiUrl, st } } - /// + /// + /// Performs a health check against the specified API endpoint. + /// + /// The full URL of the API endpoint to check. + /// `true` if the API responded with a success status code, `false` otherwise. If an error occurs while performing the check, the returned Result will be a failure containing the error message. public async Task> CheckApiHealthAsync(string apiUrl) { var startTime = DateTime.UtcNow; @@ -221,7 +246,16 @@ public async Task> CheckApiHealthAsync(string apiUrl) } } - /// + /// + /// Fetches paginated data from the specified API endpoint and returns it as a PaginatedResponseDto<T>. + /// + /// The base URL of the paginated API endpoint. + /// The page number to request. + /// The number of items per page. + /// A Result containing the paginated response DTO with items and pagination metadata on success; on failure the Result contains an error message. + /// + /// If the API response is a plain JSON array instead of a paginated structure, the array will be wrapped into a PaginatedResponseDto<T> using the provided and , with TotalPages set to 1 and TotalItems equal to the array length. + /// public async Task>> GetPaginatedDataAsync(string apiUrl, int page, int pageSize) { var startTime = DateTime.UtcNow; @@ -282,4 +316,4 @@ public async Task>> GetPaginatedDataAsync(stri } } } -} +} \ No newline at end of file diff --git a/Infrastructure/Services/TokenBlacklistService.cs b/Infrastructure/Services/TokenBlacklistService.cs index fa7b772..cca53ea 100644 --- a/Infrastructure/Services/TokenBlacklistService.cs +++ b/Infrastructure/Services/TokenBlacklistService.cs @@ -61,7 +61,12 @@ public class TokenBlacklistService : ITokenBlacklistService /// /// Repository for token persistence /// Memory cache for fast local lookups - /// Logger for security auditing and debugging + /// + /// Initializes a new instance of with the required dependencies. + /// + /// Repository used to persist and query token entities. + /// In-memory cache for storing blacklist entries for quick lookup. + /// Logger used for security auditing and debugging. public TokenBlacklistService( ITokenRepository tokenRepository, IMemoryCache memoryCache, @@ -72,7 +77,11 @@ public TokenBlacklistService( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - /// + /// + /// Blacklists the JWT identified by its JTI: revokes the corresponding token in persistence, updates the in-memory blacklist cache, and records the outcome in logs. + /// + /// The JWT string whose JTI claim will be used to identify and revoke the corresponding token. + /// A token to cancel the asynchronous operation. public async Task BlacklistTokenAsync(string jwtToken, CancellationToken cancellationToken = default) { try @@ -150,7 +159,15 @@ public async Task BlacklistTokenAsync(string jwtToken, CancellationToken cancell } } - /// + /// + /// Checks whether the provided JWT is currently blacklisted. + /// + /// The JWT to inspect; expected to contain a JTI (token identifier). + /// Cancellation token to cancel the operation. + /// `true` if the token has been revoked and is not expired, `false` otherwise. + /// + /// The method first consults an in-memory cache and then falls back to the repository. Tokens without a JTI or an empty/whitespace JWT are treated as not blacklisted. On error the method returns `false`. + /// public async Task IsTokenBlacklistedAsync(string jwtToken, CancellationToken cancellationToken = default) { try @@ -264,7 +281,10 @@ public async Task CleanupExpiredTokensAsync(CancellationToken cancellationT return cleanedCount; } - /// + /// + /// Retrieves aggregated statistics about the token blacklist including total revoked tokens, expired tokens pending cleanup, an estimated memory usage, and the timestamp when the statistics were calculated. + /// + /// A TokenBlacklistStats containing TotalBlacklistedTokens, ExpiredTokensPendingCleanup, EstimatedMemoryUsageBytes, CacheHitRatePercent (null if unavailable), and LastUpdated. public async Task GetBlacklistStatsAsync(CancellationToken cancellationToken = default) { try @@ -302,7 +322,11 @@ public async Task GetBlacklistStatsAsync(CancellationToken /// Extracts the TokenId (JTI claim) from a JWT token. /// /// The JWT token to parse - /// The token ID (JTI claim) or null if not found + /// + /// Retrieves the JWT ID (JTI) claim value from the provided JWT string. + /// + /// The compact JWT from which to extract the JTI claim. + /// The token ID (JTI claim) if present; otherwise null (including when extraction fails). private string? ExtractTokenId(string jwtToken) { try @@ -328,7 +352,11 @@ public async Task GetBlacklistStatsAsync(CancellationToken /// Generates a cache key for a blacklisted token. /// /// The JWT ID (JTI) - /// Cache key for the blacklist entry + /// +/// Generates the memory-cache key for a token blacklist entry. +/// +/// The token's JTI (unique token identifier). +/// The cache key for the blacklist entry. private static string GetBlacklistKey(string tokenId) => $"{BlacklistKeyPrefix}{tokenId}"; } -} +} \ No newline at end of file diff --git a/Presentation/Controllers/v1/AuthController.cs b/Presentation/Controllers/v1/AuthController.cs index ffbdf32..2a5068c 100644 --- a/Presentation/Controllers/v1/AuthController.cs +++ b/Presentation/Controllers/v1/AuthController.cs @@ -43,7 +43,12 @@ public class AuthController : ControllerBase /// /// Service for creating JWT tokens /// Logger for tracking token generation requests - /// MediatR instance for CQRS pattern + /// + /// Initializes a new instance of the class with required services. + /// + /// Service that creates development/demo JWT tokens. + /// Logger for the controller. + /// MediatR mediator used to dispatch authentication commands. public AuthController( JwtTokenGenerator tokenGenerator, ILogger logger, @@ -87,7 +92,18 @@ public AuthController( /// - Easy to extend with additional behaviors (validation, caching, etc.) /// /// Returns the JWT token with metadata - /// If the request is invalid + /// + /// Authenticate a user via a CQRS login command and return a JWT token and related metadata on success. + /// + /// LoginRequest containing Username, Password, and Role. Username is required. + /// + /// 200 OK with an object containing `token`, `tokenType`, `expiresIn`, `username`, `roles`, token metadata, and a message on successful authentication; + /// 400 Bad Request with an error object when validation fails or the login command reports failure; + /// 500 Internal Server Error with a generic error payload on unexpected failures. + /// + /// + /// This endpoint is intended for development/demo use. In production, authentication should be handled by a secure identity provider and passwords must be validated, hashed, and protected. + /// [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -201,7 +217,16 @@ public async Task Login([FromBody] LoginRequest request) /// /// Logout successful, token blacklisted /// No token provided or invalid token format - /// Token is already invalid or expired + /// + /// Blacklists the caller's JWT to perform a logout using a CQRS command. + /// + /// + /// An IActionResult representing the outcome: + /// 200 OK with logout details when the token is successfully blacklisted; + /// 400 Bad Request if the Authorization token is missing or blacklisting failed; + /// 401 Unauthorized if the provided token is already invalid or expired; + /// 500 Internal Server Error for unexpected server errors. + /// [HttpPost("logout")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -317,7 +342,12 @@ public async Task Logout() /// specified. /// An containing a JSON object with the generated token, token type, requested /// type, assigned roles, and usage instructions for the Authorization header.Returns the JWT token + /// + /// Generates a convenience JWT for testing and returns a minimal token payload. + /// + /// Requested token type: use "admin" (case-insensitive) to include the Admin role; any other value produces a regular user token. + /// An object containing `token` (the JWT string), `tokenType` ("Bearer"), `type` (echoed request), `roles` (assigned roles), and `usage` (an Authorization header example). + /// Intended for development and testing only; do not use as a production authentication mechanism. [HttpGet("token")] [ProducesResponseType(StatusCodes.Status200OK)] public IActionResult GetToken([FromQuery] string type = "user") @@ -377,7 +407,10 @@ public IActionResult GetToken([FromQuery] string type = "user") /// /// Extracts JWT token from the Authorization header. /// - /// JWT token string or null if not found + /// + /// Extracts a Bearer JWT from the current request's Authorization header when present and valid. + /// + /// The JWT token string when a valid Bearer token is found; otherwise null. private string? ExtractTokenFromRequest() { try @@ -481,4 +514,4 @@ public class LoginRequest /// public string Role { get; set; } = "User"; } -} +} \ No newline at end of file diff --git a/Presentation/Controllers/v1/SampleController.cs b/Presentation/Controllers/v1/SampleController.cs index de0b3ea..2d7ca4d 100644 --- a/Presentation/Controllers/v1/SampleController.cs +++ b/Presentation/Controllers/v1/SampleController.cs @@ -51,7 +51,11 @@ public class SampleController : ControllerBase /// - Testable (can mock dependencies) /// - Loosely coupled (no direct service dependencies) /// - Follows SOLID principles (Dependency Inversion) - /// + /// + /// Initializes a new instance of the SampleController with its dependencies. + /// + /// MediatR mediator used to dispatch queries and commands to handlers. + /// Structured logger for recording controller events and diagnostics. public SampleController(IMediator mediator, ILogger logger) { _mediator = mediator; @@ -88,7 +92,14 @@ public SampleController(IMediator mediator, ILogger logger) /// Returns the requested data /// If the external API call fails /// If the user is not authenticated - /// If an unexpected error occurs + /// + /// Retrieves all sample data from the configured external API. + /// + /// HTTP 200 with a collection of SampleDtoModel on success; HTTP 400 with `{ error }` when the external call fails; HTTP 401 when the caller is unauthorized; HTTP 500 with an error payload for unexpected server errors. + /// Returns a collection of SampleDtoModel. + /// When the external API or query handler reports a failure; response contains `{ error }`. + /// When the caller is not authenticated. + /// When an unexpected server error occurs; response contains a generic error message and diagnostic details. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -203,7 +214,10 @@ public async Task GetAllData( /// **Security Note:** /// This endpoint leaks minimal information (just that API is running) /// No sensitive data or system details exposed - /// + /// + /// Provides a public health-check endpoint that reports service status and the current UTC timestamp. + /// + /// An HTTP 200 OK response with a JSON object containing `status` (human-readable message) and `timestamp` (UTC DateTime). [HttpGet("status")] [AllowAnonymous] // Public endpoint for health checks (overrides [Authorize] at class level) [ProducesResponseType(StatusCodes.Status200OK)] @@ -244,6 +258,15 @@ public async Task GetAllData( /// If the external API call fails or ID is invalid /// If the user is not authenticated /// If the resource is not found + /// + /// Retrieves a single SampleDtoModel by its identifier. + /// + /// Identifier of the resource to retrieve; cannot be null, empty, or whitespace. + /// HTTP 200 with the requested resource when found; HTTP 400 with an error message for validation failures or API-reported errors; HTTP 401 if the caller is not authenticated; HTTP 404 if the resource does not exist; HTTP 500 with error details for unexpected failures. + /// Resource found and returned. + /// Invalid request or external API returned an error. + /// Authentication is required or has failed. + /// Resource with the specified ID was not found. /// If an unexpected error occurs [HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -375,7 +398,13 @@ public async Task GetDataById(string id) /// - System configuration APIs /// - Audit log access /// - Reporting and analytics + /// + /// Provides an admin-only endpoint that returns a simple confirmation payload. + /// + /// + /// Requires the caller to satisfy the "AdminOnly" authorization policy. Access is logged for audit purposes using the caller's identity. /// + /// An HTTP 200 response containing a JSON object with a confirmation message and the current user's username. [HttpGet("admin")] [Authorize(Policy = "AdminOnly")] // Only users with Admin role can access (additional authorization check) [ProducesResponseType(StatusCodes.Status200OK)] @@ -405,4 +434,4 @@ public IActionResult GetAdminData() return Ok(new { message = "This is admin-only data", user = User.Identity?.Name }); } } -} +} \ No newline at end of file diff --git a/Presentation/Controllers/v1/TokenBlacklistController.cs b/Presentation/Controllers/v1/TokenBlacklistController.cs index a7f2a37..6b36791 100644 --- a/Presentation/Controllers/v1/TokenBlacklistController.cs +++ b/Presentation/Controllers/v1/TokenBlacklistController.cs @@ -41,7 +41,12 @@ public class TokenBlacklistController : ControllerBase /// Initializes a new instance of TokenBlacklistController. /// /// MediatR instance for CQRS operations - /// Logger for audit and debugging + /// + /// Initializes a new instance of with the required MediatR mediator and logger. + /// + /// Mediator for sending CQRS queries and commands. + /// Logger for audit and debugging. + /// Thrown if or is null. public TokenBlacklistController(IMediator mediator, ILogger logger) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); @@ -77,7 +82,16 @@ public TokenBlacklistController(IMediator mediator, ILogger /// Returns detailed token blacklist status /// Invalid token format or missing token - /// Unauthorized - authentication required + /// + /// Check whether the specified JWT is present in the token blacklist and return a detailed status payload. + /// + /// The JWT to check (required). + /// If true, forces fresh evaluation rather than using a cached result. + /// + /// 200 with a payload containing: `is_blacklisted`, `token_id`, `status`, `details`, `blacklisted_at`, + /// `token_expires_at`, `checked_at`, `from_cache`, and `processing_method`; + /// 400 when the token is missing or the query fails; 401 when the caller is unauthorized; 500 on internal errors. + /// [HttpGet("status")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -172,7 +186,11 @@ public async Task GetTokenStatus([FromQuery] string token, [FromQ /// /// Returns comprehensive blacklist statistics /// Unauthorized - authentication required - /// Forbidden - Admin role required + /// + /// Retrieve comprehensive token blacklist statistics for administrative use. + /// + /// If true, force fresh statistics retrieval and bypass any cached results. + /// An HTTP response: on success a 200 OK with a JSON object containing `basic`, `performance`, `security`, `health`, and `metadata` sections; otherwise an error response. [HttpGet("stats")] [Authorize(Roles = "Admin")] // Admin-only endpoint [ProducesResponseType(StatusCodes.Status200OK)] @@ -279,7 +297,13 @@ public async Task GetBlacklistStatistics([FromQuery] bool bypassC /// suitable for automated monitoring systems. /// /// System is healthy - /// System has issues + /// + /// Performs a health check of the token blacklist system using a CQRS query and reports service health. + /// + /// + /// HTTP 200 with a JSON payload { status = "healthy", service = "token-blacklist", timestamp, method } when healthy; + /// HTTP 503 with a JSON payload { status = "unhealthy", service = "token-blacklist", timestamp, warnings } when unhealthy or with { status = "unhealthy", service = "token-blacklist", error, timestamp } on failure. + /// [HttpGet("health")] [AllowAnonymous] // Allow health checks without authentication [ProducesResponseType(StatusCodes.Status200OK)] @@ -324,4 +348,4 @@ public async Task GetHealth() } } } -} +} \ No newline at end of file diff --git a/Presentation/Extensions/DependencyInjection/ApplicationServiceExtensions.cs b/Presentation/Extensions/DependencyInjection/ApplicationServiceExtensions.cs index 581a2ca..c1d34d2 100644 --- a/Presentation/Extensions/DependencyInjection/ApplicationServiceExtensions.cs +++ b/Presentation/Extensions/DependencyInjection/ApplicationServiceExtensions.cs @@ -18,6 +18,10 @@ namespace SecureCleanApiWaf.Presentation.Extensions.DependencyInjection /// public static class ApplicationServiceExtensions { + /// + /// Registers application-layer services into the dependency injection container, including MediatR handlers, AutoMapper profiles, the ApiDataMapper, concrete closed generic request handlers for DTOs, authentication CQRS handlers, and MediatR pipeline behaviors (e.g., caching). + /// + /// The same instance with application services registered. public static IServiceCollection AddApplicationServices(this IServiceCollection services) { // Get the assembly containing Application layer code @@ -52,6 +56,13 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection return services; } + /// + /// Registers closed generic MediatR handlers for concrete DTO/model types required by the application. + /// + /// + /// Adds transient registrations that map specific request types (for example, GetApiDataQuery<T> and GetApiDataByIdQuery<T>) to their concrete handler implementations. + /// Extend this method with additional registrations when new DTO/model types are introduced (see the SampleDtoModel example). + /// private static void RegisterConcreteGenericHandlers(IServiceCollection services) { // Register closed generic handlers for each concrete type you need @@ -95,6 +106,15 @@ private static void RegisterConcreteGenericHandlers(IServiceCollection services) /// - Implement proper logging and error handling /// - Support caching through ICacheable interface where appropriate /// - Integrate with existing infrastructure services + /// + /// Registers authentication-related MediatR request handlers into the DI container. + /// + /// + /// Adds transient registrations for command and query handlers used by the authentication flow: + /// - LoginUserCommand -> LoginUserCommandHandler (IRequestHandler<LoginUserCommand, Result<LoginResponseDto>>) + /// - BlacklistTokenCommand -> BlacklistTokenCommandHandler (IRequestHandler<BlacklistTokenCommand, Result<BlacklistTokenResponse>>) + /// - IsTokenBlacklistedQuery -> IsTokenBlacklistedQueryHandler (IRequestHandler<IsTokenBlacklistedQuery, Result<TokenBlacklistStatusDto>>) + /// - GetTokenBlacklistStatsQuery -> GetTokenBlacklistStatsQueryHandler (IRequestHandler<GetTokenBlacklistStatsQuery, Result<TokenBlacklistStatisticsDto>>) /// private static void RegisterAuthenticationHandlers(IServiceCollection services) { @@ -127,4 +147,4 @@ private static void RegisterAuthenticationHandlers(IServiceCollection services) GetTokenBlacklistStatsQueryHandler>(); } } -} +} \ No newline at end of file diff --git a/Presentation/Extensions/DependencyInjection/InfrastructureServiceExtensions.cs b/Presentation/Extensions/DependencyInjection/InfrastructureServiceExtensions.cs index 2e132f1..2140427 100644 --- a/Presentation/Extensions/DependencyInjection/InfrastructureServiceExtensions.cs +++ b/Presentation/Extensions/DependencyInjection/InfrastructureServiceExtensions.cs @@ -31,6 +31,11 @@ namespace SecureCleanApiWaf.Presentation.Extensions.DependencyInjection /// public static class InfrastructureServiceExtensions { + /// + /// Registers infrastructure dependencies: configures the EF Core DbContext with SQL Server, repository and security services, HTTP clients with resilience policies and handlers, and in-memory/distributed caching. + /// + /// The same instance with infrastructure services registered. + /// Thrown when database configuration from DatabaseSettings is invalid. public static IServiceCollection AddInfrastructureServices( this IServiceCollection services, IConfiguration configuration) @@ -243,7 +248,12 @@ public static IServiceCollection AddInfrastructureServices( /// - Gives the API time to recover /// - Reduces server load during outages /// - Industry best practice for resilience - /// + /// + /// Creates a retry policy for transient HTTP failures and rate limiting responses. + /// + /// + /// An async policy that retries up to 3 times with exponential backoff (2s, 4s, 8s) for transient HTTP errors (5xx and 408) and 429 (Too Many Requests), invoking a retry callback on each attempt. + /// private static IAsyncPolicy GetRetryPolicy() { return HttpPolicyExtensions @@ -308,7 +318,15 @@ private static IAsyncPolicy GetRetryPolicy() /// - Gives failing services time to recover /// - Reduces resource consumption during outages /// - Industry standard for microservices resilience + /// + /// Creates a circuit breaker policy that protects HTTP calls by opening the circuit after repeated transient failures. + /// + /// + /// The policy treats transient HTTP errors (5xx and 408) and 429 (Too Many Requests) as failures. When the circuit opens, it remains open for 30 seconds before allowing attempts to resume. Callbacks are invoked on break and reset to surface state changes (e.g., logging or telemetry). /// + /// + /// An asynchronous circuit breaker policy that opens after 5 consecutive transient HTTP failures and stays open for 30 seconds; invokes onBreak and onReset callbacks when the circuit state changes. + /// private static IAsyncPolicy GetCircuitBreakerPolicy() { return HttpPolicyExtensions @@ -352,4 +370,4 @@ private static IAsyncPolicy GetCircuitBreakerPolicy() }); } } -} +} \ No newline at end of file diff --git a/Presentation/Extensions/DependencyInjection/PresentationServiceExtensions.cs b/Presentation/Extensions/DependencyInjection/PresentationServiceExtensions.cs index 07b73bb..505879a 100644 --- a/Presentation/Extensions/DependencyInjection/PresentationServiceExtensions.cs +++ b/Presentation/Extensions/DependencyInjection/PresentationServiceExtensions.cs @@ -32,6 +32,10 @@ namespace SecureCleanApiWaf.Presentation.Extensions.DependencyInjection /// public static class PresentationServiceExtensions { + /// + /// Registers and configures presentation-layer services into the dependency injection container, including ProblemDetails, JWT authentication, authorization policies, rate limiting, CORS, Blazor server components, API controllers, Swagger/OpenAPI, and health checks. + /// + /// The same instance with presentation services registered. public static IServiceCollection AddPresentationServices(this IServiceCollection services, IConfiguration configuration) { // =========================================================================================== @@ -445,4 +449,4 @@ public static IServiceCollection AddPresentationServices(this IServiceCollection return services; } } -} +} \ No newline at end of file diff --git a/Presentation/Extensions/HttpPipeline/WebApplicationExtensions.cs b/Presentation/Extensions/HttpPipeline/WebApplicationExtensions.cs index f4227cc..52bf204 100644 --- a/Presentation/Extensions/HttpPipeline/WebApplicationExtensions.cs +++ b/Presentation/Extensions/HttpPipeline/WebApplicationExtensions.cs @@ -35,6 +35,10 @@ namespace SecureCleanApiWaf.Presentation.Extensions.HttpPipeline /// public static class WebApplicationExtensions { + /// + /// Configures the application's HTTP request pipeline in a security-first, production-ready order and maps endpoints. + /// + /// The same instance after configuring middleware, endpoints, and security headers. public static WebApplication ConfigurePipeline(this WebApplication app) { // =========================================================================================== @@ -420,4 +424,4 @@ public static WebApplication ConfigurePipeline(this WebApplication app) return app; } } -} +} \ No newline at end of file