6 Commits

Author SHA1 Message Date
n8n IEC 62304 Bot 49def4ee2d docs(iec62304): [REL-1.0.2] apply curated XML doc review updates 2026-07-02 12:27:30 +02:00
n8n IEC 62304 Bot 7de114a95a docs(iec62304): [REL-1.0.2] validate and curate XML doc review markers 2026-07-02 12:21:44 +02:00
ljrojas91 609c601fe1 chore: ignore n8n local workflow artifacts 2026-07-02 10:56:12 +02:00
n8n IEC 62304 Bot 052918f601 docs: apply curated AI doc review updates for 1.0.2
Documentation branch: docs/rc-1.0.2
Technical branch: release/rc-1.0.2

Scope:
- Technical changed files: 13
- Technical changed C# files: 2
- Curated review markers: 33
- Kept review markers before apply: 32
- Removed false positives during curation: 1

Applied review files:
- adas-core.Infrastructure/Repositories/MasterListRepository.cs

Generated by n8n IEC 62304 documentation workflow.
2026-06-28 22:59:38 +02:00
n8n IEC 62304 Bot d51271effd merge: bring documentation baseline from docs/master into docs/rc-1.0.2 2026-06-28 22:22:56 +02:00
jrojas e6a350a426 aqui copie los archivos de la rama de guitea fix/CR-1.0.1-alineación-actualizacion-de-dependencias 2026-06-28 14:11:07 +02:00
15 changed files with 308 additions and 240 deletions
+4
View File
@@ -381,3 +381,7 @@ FodyWeavers.xsd
.idea .idea
/.ollamassist/conversations /.ollamassist/conversations
/write_readme.py /write_readme.py
# n8n local workflow artifacts
.n8n-docs-rc-flow/
logs/
@@ -21,20 +21,30 @@
<PackageReference Include="AutoMapper" Version="16.1.1" /> <PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.19.1" />
<PackageReference Include="MongoDB.Driver" Version="3.9.0" /> <PackageReference Include="MongoDB.Driver" Version="3.9.0" />
<PackageReference Include="Quartz" Version="3.18.1" /> <PackageReference Include="Quartz" Version="3.18.1" />
<PackageReference Include="StackExchange.Redis" Version="3.0.0" /> <PackageReference Include="StackExchange.Redis" Version="3.0.0" />
<PackageReference Include="System.Text.Json" Version="10.0.9" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Features">
<HintPath>..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\7.0.10\Microsoft.AspNetCore.Http.Features.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -10,12 +10,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.11" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.28" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.11" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.9" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
+4
View File
@@ -10,11 +10,15 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" /> <PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.11" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.28" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.28" />
<PackageReference Include="MongoDB.Bson" Version="3.9.0" /> <PackageReference Include="MongoDB.Bson" Version="3.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.3" /> <PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.3" />
<PackageReference Include="Serilog" Version="4.3.1" /> <PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="SharpCompress" Version="0.49.1" />
<PackageReference Include="Snappier" Version="1.3.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
<PackageReference Include="System.Text.Json" Version="10.0.9" /> <PackageReference Include="System.Text.Json" Version="10.0.9" />
<PackageReference Include="TagLibSharp" Version="2.3.0" /> <PackageReference Include="TagLibSharp" Version="2.3.0" />
@@ -79,6 +79,7 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// </summary> /// </summary>
/// <param name="entity">The entity to insert.</param> /// <param name="entity">The entity to insert.</param>
/// <exception cref="Exception">Throws and re-throws exceptions after logging.</exception> /// <exception cref="Exception">Throws and re-throws exceptions after logging.</exception>
/// <!-- aidoc:v1 sig=c7a7ad8 body=26b3581 -->
public override async Task InsertOneAsync(T entity) public override async Task InsertOneAsync(T entity)
{ {
try try
@@ -113,10 +114,9 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Persists the supplied <typeparamref name="T"/> entity in the collection by replacing the document identified by <see cref="T.Id"/>, or inserting a new document when no match is found (upsert). Logs and rethrows any exception raised during the operation. /// Upserts the specified <paramref name="entity"/> in the collection, replacing the document matched by its identifier or inserting it when no match is found. Exceptions are logged and rethrown to the caller.
/// </summary> /// </summary>
/// <typeparam name="T">The document type stored in the collection.</typeparam> /// <param name="entity">The entity to persist; its identifier is used as the filter and its state replaces the existing document or creates a new one when absent.</param>
/// <param name="entity">The entity whose <see cref="T.Id"/> locates the target document and whose state is written to the collection.</param>
/// <!-- aidoc:v1 sig=019b691 body=863650a --> /// <!-- aidoc:v1 sig=019b691 body=863650a -->
public async Task Update(T entity) public async Task Update(T entity)
{ {
@@ -167,11 +167,10 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Asynchronously retrieves an entity of type <typeparamref name="T"/> from the collection by its <see cref="ObjectId"/>, applying a projection that truncates the <c>options</c> field to 100 elements. Returns <see langword="null"/> when no matching document is found, or when an exception is caught and logged by <see cref="Log"/>. /// Asynchronously retrieves an entity of type <typeparamref name="T"/> from the collection using its <see cref="ObjectId"/>, applying a projection that truncates the "options" field to a maximum of 100 elements. Returns <c>null</c> when no document matches the identifier or when an exception is thrown, in which case the exception is logged and swallowed.
/// </summary> /// </summary>
/// <typeparam name="T">The document type stored in the collection, expected to expose an <see cref="ObjectId"/> identifier.</typeparam> /// <param name="id">The <see cref="ObjectId"/> identifier of the entity to look up.</param>
/// <param name="id">The <see cref="ObjectId"/> of the entity to locate.</param> /// <returns>A <see cref="Task{T}"/> that resolves to the matching entity of type <typeparamref name="T"/>, or <c>null</c> if the entity is not found or an error occurs during retrieval.</returns>
/// <returns>A <see cref="Task{T}"/> that resolves to the matching entity, or <see langword="null"/> if the document is not found or the operation fails.</returns>
/// <!-- aidoc:v1 sig=81ac96a body=1519944 --> /// <!-- aidoc:v1 sig=81ac96a body=1519944 -->
public async Task<T?> FindById(ObjectId id) public async Task<T?> FindById(ObjectId id)
{ {
@@ -196,12 +195,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves a single <see cref="OptionList"/> item from a master document identified by <paramref name="masterId"/> and <paramref name="optionId"/>, applying locale-aware name resolution. When <paramref name="locale"/> is <see cref="LocaleEnum.Default"/> or matches the document's default locale, the base name is returned; otherwise, the matching entry from the <c>localeItems</c> map is used, falling back to the base name when no translation is found. Returns <see langword="null"/> when the option is not found or if an error occurs during aggregation. /// Retrieves a single <see cref="OptionList"/> item by its parent master identifier and option identifier, resolving the display name according to the requested <paramref name="locale"/> using the aggregation translation logic embedded in the pipeline.
/// </summary> /// </summary>
/// <param name="masterId">The <see cref="ObjectId"/> of the master document that contains the option collection to search.</param> /// <param name="masterId">The <see cref="MongoDB.Bson.ObjectId"/> of the master document whose embedded options collection will be searched.</param>
/// <param name="optionId">The <see cref="ObjectId"/> of the specific option within the master document's options array.</param> /// <param name="optionId">The <see cref="MongoDB.Bson.ObjectId"/> of the specific <see cref="OptionList"/> to retrieve from the unwound options array.</param>
/// <param name="locale">The <see cref="LocaleEnum"/> value that drives translation selection; <see cref="LocaleEnum.Default"/> resolves to the document's default locale, while any other value attempts a locale-specific lookup.</param> /// <param name="locale">The <see cref="LocaleEnum"/> used to select the localized name; when equal to <see cref="LocaleEnum.Default"/> or matching the document's default locale, the base name is returned, otherwise the matching <c>localeItems</c> entry is used with a fallback to the base name.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the matched <see cref="OptionList"/>, or <see langword="null"/> if no document/option is found or the operation fails.</returns> /// <returns>A <see cref="Task"/> that yields the matching <see cref="OptionList"/> deserialized from the aggregation result, or <c>null</c> if no document is found or the operation fails.</returns>
/// <!-- aidoc:v1 sig=fdb2628 body=0e4983e --> /// <!-- aidoc:v1 sig=fdb2628 body=0e4983e -->
public async Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId, LocaleEnum locale) public async Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId, LocaleEnum locale)
{ {
@@ -342,12 +341,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Finds a specific option within a master list by master and option IDs without locale translation. /// Asynchronously retrieves the <see cref="OptionList"/> option that matches the supplied <paramref name="masterId"/> and <paramref name="optionId"/> from the parent document's options array, returning the first match or <see langword="null"/> when no option is found or when an error is logged.
/// </summary> /// </summary>
/// <param name="masterId">The ObjectId of the master list.</param> /// <param name="masterId">The identifier of the master document that owns the options array.</param>
/// <param name="optionId">The ObjectId of the option to retrieve.</param> /// <param name="optionId">The identifier of the specific option to locate within that array.</param>
/// <returns>The OptionList if found; otherwise, null.</returns> /// <returns>A <see cref="Task{T}"/> that yields the matching <see cref="OptionList"/>, or <see langword="null"/> if the lookup yields no result or the operation fails.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception>
/// <!-- aidoc:v1 sig=ccc34cb body=71837cc --> /// <!-- aidoc:v1 sig=ccc34cb body=71837cc -->
public async Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId) public async Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId)
{ {
@@ -374,11 +372,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Asynchronously finds an entity of type <typeparamref name="T"/> by its MongoDB <see cref="ObjectId"/>, executing an aggregation pipeline that unwinds the <c>options</c> array and resolves localized option names. When <paramref name="locale"/> is <c>null</c> or <see cref="LocaleEnum.Default"/>, the original option name is returned; otherwise, the option name is translated using the supplied locale, falling back to the original name when no matching <c>localeItems</c> entry exists. Returns <c>null</c> when no document is found or when an exception is caught and logged. /// Retrieves an entity by its <see cref="ObjectId"/>, applying locale-aware name resolution to the nested options. When <paramref name="locale"/> is <see cref="LocaleEnum.Default"/> or matches the document's default locale, the original option name is used; otherwise the translated name is looked up in <c>localeItems</c> with a fallback to the original name. Returns <c>null</c> when the entity is not found or an error occurs.
/// </summary> /// </summary>
/// <param name="id">The MongoDB <see cref="ObjectId"/> that uniquely identifies the document to retrieve.</param> /// <param name="id">The <see cref="ObjectId"/> of the document to retrieve.</param>
/// <param name="locale">The optional <see cref="LocaleEnum"/> used to resolve translated option names; pass <c>null</c> or <see cref="LocaleEnum.Default"/> to use the default (non-translated) names.</param> /// <param name="locale">The optional <see cref="LocaleEnum"/> used to resolve translated option names. When <c>null</c> or <see cref="LocaleEnum.Default"/>, the default locale is applied.</param>
/// <returns>A <see cref="Task{T}"/> that yields the matched entity of type <typeparamref name="T"/>, or <c>null</c> if no document matches <paramref name="id"/> or if an error occurs during aggregation.</returns> /// <returns>A <see cref="Task{T}"/> containing the localized entity, or <c>null</c> when the entity cannot be found or the aggregation fails.</returns>
/// <!-- aidoc:v1 sig=cb4a658 body=f2e88d8 --> /// <!-- aidoc:v1 sig=cb4a658 body=f2e88d8 -->
public async Task<T?> FindById(ObjectId id, LocaleEnum? locale) public async Task<T?> FindById(ObjectId id, LocaleEnum? locale)
{ {
@@ -557,10 +555,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Asynchronously finds a single <typeparamref name="T"/> entity by its <see cref="T.Name"/> using a MongoDB query, projecting only the first 100 elements of the <c>options</c> field to limit payload size. Returns <c>null</c> if no matching document is found or if an error occurs during the search. /// Asynchronously finds the first entity matching the specified <paramref name="name"/> in the underlying collection,
/// applying a projection that limits the <c>options</c> array to 100 elements. Returns null when no matching document is found,
/// and logs the error and returns null if the lookup throws an exception.
/// </summary> /// </summary>
/// <param name="name">The name value used to build the equality filter against the entity's <c>Name</c> field.</param> /// <param name="name">The value compared against the entity's <c>Name</c> field to locate the document.</param>
/// <returns>A <see cref="Task{T}"/> that yields the matching <typeparamref name="T"/> instance, or <c>null</c> when no document matches or the operation fails.</returns> /// <returns>A task that yields the first matching entity of type T, or null if no match is found or the operation fails.</returns>
/// <!-- aidoc:v1 sig=80a1541 body=8b5b558 --> /// <!-- aidoc:v1 sig=80a1541 body=8b5b558 -->
public async Task<T?> FindByName(string name) public async Task<T?> FindByName(string name)
{ {
@@ -597,10 +597,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves paginated master lists with optional text filtering. /// Builds a query for the master list of <typeparamref name="T"/> documents, sorted ascending by the name field.
/// When <paramref name="filter"/> carries non-empty search text, a case-insensitive regex match is applied to the Name property; otherwise the query is returned without additional filtering.
/// </summary> /// </summary>
/// <param name="filter">The pagination and filtering parameters.</param> /// <param name="filter">The <see cref="PaginationFilter"/> providing pagination settings and the optional search text.</param>
/// <returns>A fluent queryable for MasterList results.</returns> /// <returns>An <see cref="IFindFluent{T,T}"/> representing the prepared query, ready for further pagination and execution.</returns>
/// <!-- aidoc:v1 sig=bbb39c7 body=f42ebaa --> /// <!-- aidoc:v1 sig=bbb39c7 body=f42ebaa -->
public IFindFluent<T, T> GetPaginatedMasterList(PaginationFilter filter) public IFindFluent<T, T> GetPaginatedMasterList(PaginationFilter filter)
{ {
@@ -624,11 +625,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves the options belonging to the master list identified by <paramref name="listId"/>, optionally narrowed down by the text contained in <paramref name="filter"/>. Returns an empty collection when no master list exists for the given identifier, and when a non-empty filter text is supplied, applies a case-insensitive match against each option's name, description, or option type. /// Retrieves the options belonging to the master list identified by <paramref name="listId"/>, optionally applying a case-insensitive text filter on the option name, description, or type. Returns an empty list when the master list cannot be found.
/// </summary> /// </summary>
/// <param name="filter">The <see cref="PaginationFilter"/> that provides the optional text used to filter the returned options.</param> /// <param name="filter">A <see cref="PaginationFilter"/> whose <c>FilteredRequest.Text</c> is used to match option fields when provided.</param>
/// <param name="listId">The <see cref="ObjectId"/> of the master list whose options should be retrieved.</param> /// <param name="listId">The identifier of the master <see cref="OptionList"/> whose options are returned.</param>
/// <returns>A <see cref="Task"/> that resolves to a <see cref="List{OptionList}"/> of options matching the filter, or an empty list when the master list cannot be found.</returns> /// <returns>A task that yields a <see cref="List{OptionList}"/> of options matching the filter, or an empty list if the master list does not exist.</returns>
/// <!-- aidoc:v1 sig=2ac8d75 body=b25fb18 --> /// <!-- aidoc:v1 sig=2ac8d75 body=b25fb18 -->
public async Task<List<OptionList>> GetPaginatedOptions(PaginationFilter filter, ObjectId listId) public async Task<List<OptionList>> GetPaginatedOptions(PaginationFilter filter, ObjectId listId)
{ {
@@ -649,11 +650,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Adds a new <see cref="OptionList"/> entry to the master list identified by <paramref name="id"/>. Returns the created option on success, or <c>null</c> if a matching option already exists, the master list cannot be retrieved, the update does not modify any document, or the operation throws an exception that is logged. /// Adds a new <see cref="OptionList"/> entry to the master list identified by <paramref name="id"/>, rejecting duplicates that match the search criteria in <paramref name="opt"/>.
/// When <see cref="FilterOptionListElement.IsFromAdmPanel"/> is set, the provided locale items are reused; otherwise new locale entries are derived from the current default locale (falling back to <see cref="LocaleEnum.Es"/>) through <c>GetNewItemLocale</c>.
/// Returns <see langword="null"/> when the option already exists, the update modifies no documents, or an exception is caught and logged.
/// </summary> /// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list to update.</param> /// <param name="id">The identifier of the master list to which the option will be appended.</param>
/// <param name="opt">The <see cref="FilterOptionListElement"/> describing the option to add; its locale items are reused when <see cref="FilterOptionListElement.IsFromAdmPanel"/> is <c>true</c>, otherwise new locale entries are generated from the master list's <see cref="LocaleEnum"/> default.</param> /// <param name="opt">The option payload, including name, type, visual properties, and locale items used to seed translations.</param>
/// <returns>A <see cref="Task{OptionList}"/> that resolves to the newly created <see cref="OptionList"/> when the push update succeeds, or <c>null</c> when the operation is skipped, no document is modified, or the update fails.</returns> /// <returns>The persisted <see cref="OptionList"/> on success; otherwise, <see langword="null"/> if a duplicate is found, no document is modified, or an error occurs.</returns>
/// <!-- aidoc:v1 sig=1602f0d body=e7fd138 --> /// <!-- aidoc:v1 sig=1602f0d body=e7fd138 -->
public async Task<OptionList?> AddOptionToMasterList(ObjectId id, FilterOptionListElement opt) public async Task<OptionList?> AddOptionToMasterList(ObjectId id, FilterOptionListElement opt)
{ {
@@ -694,10 +697,10 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Asynchronously retrieves all entities from the underlying collection. /// Asynchronously retrieves all entities from the underlying collection by matching every document.
/// If an exception occurs during retrieval, the error is logged and an empty collection is returned rather than propagating the failure. /// If the retrieval fails, the exception is logged and an empty collection is returned as a fallback.
/// </summary> /// </summary>
/// <returns>A <see cref="Task"/> that yields an <see cref="IEnumerable{T}"/> containing all retrieved entities, or an empty collection if the operation fails.</returns> /// <returns>A task that yields an <see cref="System.Collections.Generic.IEnumerable{T}"/> of all entities, or an empty collection when an error occurs.</returns>
/// <!-- aidoc:v1 sig=3a61c61 body=40d7129 --> /// <!-- aidoc:v1 sig=3a61c61 body=40d7129 -->
public async Task<IEnumerable<T>> GetAll() public async Task<IEnumerable<T>> GetAll()
{ {
@@ -714,9 +717,10 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves all master list entries from the underlying collection without applying any filter and projects each result into a <see cref="MasterListDto"/>, where the <see cref="MasterListDto.Options"/> property is set to the count of associated options rather than the option list itself. /// Asynchronously retrieves all <see cref="MasterListDto"/> entries from the collection, mapping each result to include its identifier, name, description, list type, and the count of associated options.
/// If an error occurs while accessing the underlying store, the exception is logged and an empty collection is returned.
/// </summary> /// </summary>
/// <returns>A <see cref="Task"/> that resolves to an <see cref="IEnumerable{T}"/> containing the projected <see cref="MasterListDto"/> instances, or an empty collection if an error is encountered while querying the data store.</returns> /// <returns>A <see cref="Task{TResult}"/> that yields an <see cref="IEnumerable{MasterListDto}"/> containing the projected master list entries, or an empty collection when the operation fails.</returns>
/// <!-- aidoc:v1 sig=e493435 body=1c299ba --> /// <!-- aidoc:v1 sig=e493435 body=1c299ba -->
public async Task<IEnumerable<MasterListDto>> GetAllWithoutOptions() public async Task<IEnumerable<MasterListDto>> GetAllWithoutOptions()
{ {
@@ -742,12 +746,9 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Asynchronously counts all entities in the underlying <see cref="Collection"/> by invoking /// Asynchronously counts all entities in the underlying collection by invoking CountDocumentsAsync with a filter that matches every document. If an exception occurs during the count operation, the error is logged and the method returns <c>0</c> as a safe fallback.
/// <c>CountDocumentsAsync</c> with a filter that matches every document. If the operation fails,
/// the exception is logged via <c>Log.Error</c> and the method returns <c>0</c> as a safe fallback.
/// </summary> /// </summary>
/// <returns>A <see cref="Task{Int32}"/> that represents the asynchronous count operation. The result /// <returns>A <see cref="Task{Int32}"/> that resolves to the total number of entities in the collection, or <c>0</c> if the operation fails.</returns>
/// is the total number of entities, or <c>0</c> if an error occurred while querying the collection.</returns>
/// <!-- aidoc:v1 sig=f1f0a98 body=01cd3d1 --> /// <!-- aidoc:v1 sig=f1f0a98 body=01cd3d1 -->
public async Task<int> Count() public async Task<int> Count()
{ {
@@ -764,11 +765,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves the options of a master list identified by <paramref name="id"/>, applying the optional <paramref name="filters"/> to restrict the results by option type, text, name, or description, and resolving the localized <c>name</c> and <c>description</c> fields for each option based on the requested locale. When <paramref name="filters"/> is <c>null</c>, no options are returned. Text searches are performed case-insensitively against both the name and description of each option using a normalized pattern, and the final result is ordered alphabetically by name. Any exception during execution is logged and an empty list is returned. /// Retrieves the master list of <see cref="OptionList"/> entries associated with the supplied <paramref name="id"/>, applying the optional search and locale filters.
/// When <paramref name="filters"/> is <c>null</c> the method short-circuits and returns an empty list; if the <see cref="FilterOptionListElement.Text"/> is provided it takes precedence over <see cref="FilterOptionListElement.Name"/> and <see cref="FilterOptionListElement.Description"/> and is matched as a case-insensitive regex across name and description.
/// The pipeline resolves per-option translations for the requested locale, falling back to the original values when no translation exists, then filters, sorts by name, and returns the result — or an empty list if an exception is encountered.
/// </summary> /// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list whose options will be queried.</param> /// <param name="id">The <see cref="ObjectId"/> of the master list document to match in the aggregation pipeline.</param>
/// <param name="filters">Optional <see cref="FilterOptionListElement"/> containing the search criteria (option type, text, name, description, and locale). When <c>null</c>, the method returns an empty list.</param> /// <param name="filters">The optional <see cref="FilterOptionListElement"/> carrying the search criteria and locale to apply; when <c>null</c> no lookup is performed.</param>
/// <returns>A <see cref="Task{List{OptionList}}"/> containing the filtered, localized, and alphabetically ordered options; an empty list is returned when no filters are provided, no options match, or an error occurs.</returns> /// <returns>A <see cref="Task{List{OptionList}}"/> containing the filtered and locale-resolved options ordered by name, or an empty list when <paramref name="filters"/> is <c>null</c> or the aggregation fails.</returns>
/// <!-- aidoc:v1 sig=f6ee5be body=17d6a96 --> /// <!-- aidoc:v1 sig=f6ee5be body=17d6a96 -->
public async Task<List<OptionList>> GetMasterListByIdAndSearchOptions(ObjectId id, FilterOptionListElement? filters) public async Task<List<OptionList>> GetMasterListByIdAndSearchOptions(ObjectId id, FilterOptionListElement? filters)
{ {
@@ -978,12 +981,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates an option within a master list, handling duplicate detection, locale-specific translations, and conditional field updates based on the master list's required field rules. /// Updates an existing option within a master list identified by <paramref name="id"/>, applying the changes from <paramref name="newOpt"/> in the specified <paramref name="locale"/>. Prevents duplicates by short-circuiting when an option with the same name already exists, routes name changes to either the main field or the locale-specific translation entry depending on whether the supplied locale matches the master's default locale, and respects the per-field required flags defined on the master.
/// </summary> /// </summary>
/// <param name="id">The <see cref="MongoDB.Bson.ObjectId"/> of the master list that contains the option to update.</param> /// <param name="id">The <see cref="ObjectId"/> of the master list that contains the option to update.</param>
/// <param name="newOpt">The <see cref="OptionList"/> containing the new values to apply to the existing option.</param> /// <param name="newOpt">The <see cref="OptionList"/> carrying the new values to be applied to the matching option.</param>
/// <param name="locale">The <see cref="LocaleEnum"/> indicating which locale the <paramref name="newOpt"/> applies to; when it matches the master's default locale, the main field is updated, otherwise the translation stored in <c>localeItems</c> is updated.</param> /// <param name="locale">The <see cref="LocaleEnum"/> that determines whether the name is written to the main field or stored as a translation under <c>localeItems</c>.</param>
/// <returns>A <see cref="Task{T}"/> containing the updated <see cref="OptionList"/>, the existing option when a duplicate is detected, or <c>null</c> when the option is not found, no fields qualify for update, or the update operation fails.</returns> /// <returns>A <see cref="Task{T}"/> that yields the updated <see cref="OptionList"/> when the operation succeeds, an existing duplicate when one is detected, or <see langword="null"/> when there is nothing to update, no document was modified, or an error is logged and swallowed.</returns>
/// <!-- aidoc:v1 sig=e771f23 body=5520a4a --> /// <!-- aidoc:v1 sig=e771f23 body=5520a4a -->
public async Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt, LocaleEnum locale) public async Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt, LocaleEnum locale)
{ {
@@ -1079,11 +1082,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates an existing option inside the master list identified by <paramref name="id"/> with the data from <paramref name="newOpt"/>. Returns <see langword="null"/> when the master list is not found, when no document is modified, or when an exception is caught and logged. /// Updates an existing option within the master list identified by <paramref name="id"/> with the values supplied in <paramref name="newOpt"/>.
/// Returns <see langword="null"/> when the master list cannot be found, when no document is modified, or when the operation fails.
/// </summary> /// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list that owns the option to update.</param> /// <param name="id">The identifier of the master list that contains the option to update.</param>
/// <param name="newOpt">The <see cref="OptionList"/> whose <see cref="ObjectId"/> targets the option to replace and whose values are persisted to the document.</param> /// <param name="newOpt">The new option values to persist; its <see cref="OptionList.Id"/> is used to match the existing element.</param>
/// <returns>A task containing the updated <see cref="OptionList"/> when the update succeeds, or <see langword="null"/> when the master list is missing, nothing was modified, or the update failed.</returns> /// <returns>The updated <see cref="OptionList"/> item, or <see langword="null"/> if the master list is not found, the option was not updated, or an error occurred.</returns>
/// <!-- aidoc:v1 sig=6def54c body=9554543 --> /// <!-- aidoc:v1 sig=6def54c body=9554543 -->
public async Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt) public async Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt)
{ {
@@ -1139,11 +1143,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates the option details of a master list document identified by <paramref name="id"/>, persisting only the non-null fields supplied in <paramref name="opt"/>. /// Asynchronously updates the option details of a master list document identified by <paramref name="id"/>,
/// applying only the non-null fields supplied in <paramref name="opt"/> (such as <see cref="UpdateMasterListDetailsDto.CanAddElement"/> and <see cref="UpdateMasterListDetailsDto.OptionListDetails"/>).
/// Returns the updated <see cref="UpdateMasterListDetailsDto"/> when the document is modified, or <see langword="null"/> when no document matches the identifier, no fields were changed, or an error occurs while updating.
/// </summary> /// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list entry to update.</param> /// <param name="id">The <see cref="ObjectId"/> identifying the master list document to update.</param>
/// <param name="opt">The <see cref="UpdateMasterListDetailsDto"/> whose non-null <see cref="UpdateMasterListDetailsDto.CanAddElement"/> and <see cref="UpdateMasterListDetailsDto.OptionListDetails"/> values are written to the document.</param> /// <param name="opt">The <see cref="UpdateMasterListDetailsDto"/> containing the new values to apply; only non-null properties are included in the update.</param>
/// <returns>The supplied <see cref="UpdateMasterListDetailsDto"/> when at least one field is modified, or <see langword="null"/> when the document is not found, no changes are applied, or the operation fails.</returns> /// <returns>A <see cref="Task{T}"/> that resolves to the updated <see cref="UpdateMasterListDetailsDto"/> on success, or <see langword="null"/> when the document is not found, not modified, or the operation fails.</returns>
/// <!-- aidoc:v1 sig=53d4179 body=49f50ca --> /// <!-- aidoc:v1 sig=53d4179 body=49f50ca -->
public async Task<UpdateMasterListDetailsDto?> UpdateOptionDetailsToMasterList(ObjectId id, public async Task<UpdateMasterListDetailsDto?> UpdateOptionDetailsToMasterList(ObjectId id,
UpdateMasterListDetailsDto opt) UpdateMasterListDetailsDto opt)
@@ -1174,12 +1180,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates the name of the master list document identified by <paramref name="id"/>. /// Updates the name of a master list entry identified by <paramref name="id"/>.
/// Returns <c>true</c> if a document was modified, <c>false</c> if no document matched the id or if an exception was caught and logged. /// Returns <c>true</c> when a document was modified, or <c>false</c> when no document matched the filter or an exception is caught and logged.
/// </summary> /// </summary>
/// <param name="id">The <see cref="MongoDB.Bson.ObjectId"/> of the master list entry to update.</param> /// <param name="id">The <see cref="ObjectId"/> of the document to update.</param>
/// <param name="name">The new name to assign to the entry.</param> /// <param name="name">The new name to assign to the document.</param>
/// <returns>A <see cref="Task{Boolean}"/> that resolves to <c>true</c> when the update modified a document; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if the update modified a document; otherwise, <c>false</c>.</returns>
/// <!-- aidoc:v1 sig=5b79a51 body=36dfe99 --> /// <!-- aidoc:v1 sig=5b79a51 body=36dfe99 -->
public async Task<bool> UpdateMasterListName(ObjectId id, string name) public async Task<bool> UpdateMasterListName(ObjectId id, string name)
{ {
@@ -1201,12 +1207,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates the <see cref="T.Description"/> field of the master list entry identified by <paramref name="id"/>. /// Updates the description of the master list entry identified by <paramref name="id"/>.
/// Returns <c>true</c> when the matched document is modified, and <c>false</c> when no document matches the filter or the underlying update operation throws, in which case the exception is logged and the call resolves to <c>false</c>.
/// </summary> /// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list entry to update.</param> /// <param name="id">The <see cref="ObjectId"/> of the master list entry to update.</param>
/// <param name="description">The new description value to set on the entry.</param> /// <param name="description">The new description to apply to the entry.</param>
/// <returns>A <see cref="Task{Boolean}"/> that yields <c>true</c> if the update modified a document; otherwise, <c>false</c>.</returns> /// <returns><see langword="true"/> when the document was modified; <see langword="false"/> when no document matched the filter or the update operation failed.</returns>
/// <!-- aidoc:v1 sig=5a27a55 body=3552f46 --> /// <!-- aidoc:v1 sig=5a27a55 body=3552f46 -->
public async Task<bool> UpdateMasterListDescription(ObjectId id, string description) public async Task<bool> UpdateMasterListDescription(ObjectId id, string description)
{ {
@@ -1228,12 +1233,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Removes an option from a master list by matching all its properties. /// Removes the specified <paramref name="oldOpt"/> entry from the <c>options</c> array of a master list document identified by <paramref name="id"/>. The option is matched by all of its defining properties (name, optionType, iconDefault, iconCategory, iconColor, color, bgColor, isDefault, and description) using a pull filter, and the method returns <see langword="true"/> only when at least one document was modified; if no matching document is found, or if an exception is caught, it is logged and the method returns <see langword="false"/>.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The <see cref="MongoDB.Bson.ObjectId"/> of the master list document to update.</param>
/// <param name="oldOpt">The OptionList to remove.</param> /// <param name="oldOpt">The <see cref="OptionList"/> whose properties are used to identify which element to pull from the <c>options</c> array.</param>
/// <returns>True if the option was removed; otherwise, false.</returns> /// <returns>A <see cref="Task{Boolean}"/> that resolves to <see langword="true"/> if the document was modified (i.e., the option was removed), or <see langword="false"/> if no document was updated or an error occurred.</returns>
/// <exception cref="Exception">Logs errors and returns false on failure.</exception>
/// <!-- aidoc:v1 sig=8de50cd body=d974b9b --> /// <!-- aidoc:v1 sig=8de50cd body=d974b9b -->
public async Task<bool> RemoveMasterListOption(ObjectId id, OptionList oldOpt) public async Task<bool> RemoveMasterListOption(ObjectId id, OptionList oldOpt)
{ {
@@ -1263,11 +1267,8 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Creates background MongoDB indexes on the <see cref="DiagnosisList"/> collection to optimize queries against the nested <c>options</c> document fields, configuring the indexes with <c>spanish</c> as both the default and override language for text indexing. /// Creates MongoDB indexes for the <see cref="DiagnosisList"/> entity type, adding non-unique background indexes on the <c>options.name</c>, <c>options.description</c>, and <c>options._id</c> fields with Spanish configured as both the default and override language. If the generic type is not <see cref="DiagnosisList"/>, no indexes are created.
/// </summary> /// </summary>
/// <remarks>
/// When the generic type parameter is not <see cref="DiagnosisList"/>, the method performs no actions and returns without creating any indexes.
/// </remarks>
/// <!-- aidoc:v1 sig=4955da2 body=2c8d1f7 --> /// <!-- aidoc:v1 sig=4955da2 body=2c8d1f7 -->
public override async Task CreateIndexes() public override async Task CreateIndexes()
{ {
@@ -1301,12 +1302,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Searches for options within a master list using text search with accent-aware regex. /// Retrieves the options of a referenced entity whose <c>name</c> or <c>description</c> match the supplied <paramref name="textSearch"/>, building a case-insensitive regex pattern (via <c>BuildRegexPattern</c>) and filtering through a MongoDB aggregation pipeline against <see cref="Collection"/>. Returns an empty list when <paramref name="textSearch"/> is null, empty, or whitespace, when the entity identified by <paramref name="id"/> cannot be found, or when no options match; any error is logged and an empty list is returned.
/// </summary> /// </summary>
/// <param name="textSearch">Optional text to search within options.</param> /// <param name="textSearch">The free-text query used to match option names and descriptions. A null, empty, or whitespace value short-circuits the search and yields no results.</param>
/// <param name="id">Optional master list ObjectId to filter results.</param> /// <param name="id">The <see cref="ObjectId"/> of the parent entity whose options will be searched. When <c>null</c>, the aggregation matches a literal <c>null</c> identifier.</param>
/// <returns>A list of matching OptionList items ordered by name.</returns> /// <returns>A <see cref="Task{List}"/> containing the matching <see cref="OptionList"/> entries ordered by name, or an empty list when nothing matches or the operation fails.</returns>
/// <exception cref="Exception">Logs errors and returns empty list on failure.</exception>
/// <!-- aidoc:v1 sig=6ebeac8 body=1d7bbb7 --> /// <!-- aidoc:v1 sig=6ebeac8 body=1d7bbb7 -->
private async Task<List<OptionList>> GetOptionsByTextSearch(string? textSearch, ObjectId? id = null) private async Task<List<OptionList>> GetOptionsByTextSearch(string? textSearch, ObjectId? id = null)
{ {
@@ -1380,11 +1380,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Creates a find query against the underlying collection by combining the provided <paramref name="filters"/> with a logical AND and applying the specified <paramref name="sort"/>. When the filter list is empty, an empty filter is used so that the query matches every document. /// Creates an <see cref="IFindFluent{T,T}"/> query by combining the provided <paramref name="filters"/> with a logical AND and applying the given <paramref name="sort"/>. When <paramref name="filters"/> is empty, <see cref="MongoDB.Driver.Builders{TDocument}.Filter.Empty"/> is used so that all documents are matched.
/// </summary> /// </summary>
/// <param name="filters">The list of <see cref="FilterDefinition{T}"/> predicates to combine; an empty list causes no filtering to be applied.</param> /// <param name="filters">The list of <see cref="FilterDefinition{T}"/> instances to combine. When the list is empty, no filtering is applied.</param>
/// <param name="sort">The <see cref="SortDefinition{T}"/> used to order the returned documents.</param> /// <param name="sort">The <see cref="SortDefinition{T}"/> to apply to the resulting query.</param>
/// <returns>An <see cref="IFindFluent{T,T}"/> configured with the combined filter and sort, ready for further chaining.</returns> /// <returns>An <see cref="IFindFluent{T,T}"/> configured with the combined filter and sort.</returns>
/// <!-- aidoc:v1 sig=704d715 body=2a35652 --> /// <!-- aidoc:v1 sig=704d715 body=2a35652 -->
private IFindFluent<T, T> CreateFindFluent(List<FilterDefinition<T>> filters, SortDefinition<T> sort) private IFindFluent<T, T> CreateFindFluent(List<FilterDefinition<T>> filters, SortDefinition<T> sort)
{ {
@@ -1395,12 +1395,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Generates new locale items for a master list option based on a default locale. /// Builds a new <see cref="Locale"/> by assigning a fresh <see cref="LocaleItem"/> (with <see cref="LocaleItem.Name"/> set to <paramref name="opt"/>) to every property of <see cref="Locale"/> whose name matches a <see cref="LocaleEnum"/> value, skipping <see cref="LocaleEnum.Default"/> and the value supplied through <paramref name="localeList"/>.
/// Creates LocaleItem entries for all locales except the specified default.
/// </summary> /// </summary>
/// <param name="localeList">The default locale to exclude from translations.</param> /// <param name="localeList">The <see cref="LocaleEnum"/> value identifying the locale to omit when populating the resulting <see cref="Locale"/>.</param>
/// <param name="opt">The option name to use as default translation.</param> /// <param name="opt">The name assigned to each created <see cref="LocaleItem"/>.</param>
/// <returns>A Locale object with translations for all other locales.</returns> /// <returns>A <see cref="Locale"/> with a new <see cref="LocaleItem"/> for every locale other than <see cref="LocaleEnum.Default"/> and <paramref name="localeList"/>; properties without a matching enum name are left unset.</returns>
/// <!-- aidoc:v1 sig=147db59 body=9b63847 --> /// <!-- aidoc:v1 sig=147db59 body=9b63847 -->
private Locale GetNewItemLocale(LocaleEnum localeList, string opt) private Locale GetNewItemLocale(LocaleEnum localeList, string opt)
{ {
@@ -1436,11 +1435,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
// Método auxiliar para construir patrones regex con soporte de acentos // Método auxiliar para construir patrones regex con soporte de acentos
/// Builds a regex pattern that matches accented and non-accented versions of vowels. /// <summary>
/// Supports Spanish accent handling (á, é, í, ó, ú) and digits. /// Builds a case-insensitive regex pattern from <paramref name="input"/>, expanding each vowel into a character class that includes its common accented variants (e.g. <c>a</c> → <c>[aáàäâ]</c>). Digits are wrapped in a character class as literal matches, and any other character is appended as-is to be treated literally by the regex engine.
/// </summary> /// </summary>
/// <param name="input">The input string to build the pattern from.</param> /// <param name="input">The source text whose characters are translated into the resulting regex pattern.</param>
/// <returns>A regex-compatible pattern string.</returns> /// <returns>A <see cref="System.String"/> containing the constructed regular expression pattern.</returns>
/// <!-- aidoc:v1 sig=2992c00 body=ae88daf --> /// <!-- aidoc:v1 sig=2992c00 body=ae88daf -->
private static string BuildRegexPattern(string input) private static string BuildRegexPattern(string input)
{ {
@@ -1475,12 +1474,16 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Asynchronously retrieves a filtered list of <see cref="OptionList"/> entries from the master collection by matching the document with the specified <paramref name="id"/> and filtering options whose resolved name equals <paramref name="newOptName"/> for the given <paramref name="locale"/>. When the requested locale differs from the document's default, the pipeline uses the translated name from <c>localeItems</c>, falling back to the original name if no translation is available. Returns an empty list if no matching options are found or if an exception is logged during execution. /// Retrieves the options of a master list document identified by <paramref name="id"/>, rebuilding each option's
/// <c>Name</c> by selecting the translation matching <paramref name="locale"/> (or falling back to the original name
/// when the locale is the default or no translation exists), and filters the resulting options by exact match against
/// <paramref name="newOptName"/>. Returns the matched options ordered by name, or an empty list if no document is
/// found, no option matches, or an error occurs.
/// </summary> /// </summary>
/// <param name="id">The <see cref="ObjectId"/> identifying the master document to aggregate.</param> /// <param name="id">MongoDB <see cref="ObjectId"/> of the master list document to search.</param>
/// <param name="newOptName">The option name used to filter the options after locale-based name resolution.</param> /// <param name="newOptName">Option name (already resolved against the requested locale) used to filter the options.</param>
/// <param name="locale">The <see cref="LocaleEnum"/> value used to select the translated name for each option.</param> /// <param name="locale">Target <see cref="LocaleEnum"/> used to pick the translated name; the lowercased enum value is matched against the stored locale code.</param>
/// <returns>A <see cref="Task"/> containing a <see cref="List{T}"/> of <see cref="OptionList"/> entries ordered by name, or an empty list when no match exists or an error is caught.</returns> /// <returns>A <see cref="Task{T}"/> of <see cref="List{T}"/> of <see cref="OptionList"/> containing the matching options sorted by name, or an empty list when there is no match or an error is logged.</returns>
/// <!-- aidoc:v1 sig=342dc63 body=21f31fc --> /// <!-- aidoc:v1 sig=342dc63 body=21f31fc -->
private async Task<List<OptionList>> GetMasterListByIdAndTextSearch( private async Task<List<OptionList>> GetMasterListByIdAndTextSearch(
ObjectId id, string newOptName, LocaleEnum locale) ObjectId id, string newOptName, LocaleEnum locale)
@@ -14,6 +14,8 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" /> <PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" />
<PackageReference Include="MongoMigrations.Core" Version="4.0.15" /> <PackageReference Include="MongoMigrations.Core" Version="4.0.15" />
<PackageReference Include="StackExchange.Redis" Version="3.0.0" /> <PackageReference Include="StackExchange.Redis" Version="3.0.0" />
+153 -126
View File
@@ -5,12 +5,11 @@ using adas_core.Domain.Models.MongoModels;
using adas_core.LdapLogin.Configuration; using adas_core.LdapLogin.Configuration;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Bson; using MongoDB.Bson;
using System.DirectoryServices.Protocols; using Novell.Directory.Ldap;
using System.Net;
using Authorization = adas_core.Domain.Models.MongoModels.Authorization;
namespace adas_core.LdapLogin; namespace adas_core.LdapLogin;
@@ -20,7 +19,6 @@ namespace adas_core.LdapLogin;
/// The service also handles the creation of new users in the application if they do not already exist, based on the LDAP information. /// The service also handles the creation of new users in the application if they do not already exist, based on the LDAP information.
/// It uses configuration settings for connecting to the LDAP server and for mapping LDAP attributes to user properties and authorities. /// It uses configuration settings for connecting to the LDAP server and for mapping LDAP attributes to user properties and authorities.
/// </summary> /// </summary>
/// <!-- aidoc:v1 sig=4e5637f -->
public class LdapLoginService : ILoginService public class LdapLoginService : ILoginService
{ {
/// <summary> /// <summary>
@@ -52,7 +50,6 @@ public class LdapLoginService : ILoginService
/// <param name="userService">The user service for managing user information.</param> /// <param name="userService">The user service for managing user information.</param>
/// <param name="authorityService">The authority service for managing user authorities.</param> /// <param name="authorityService">The authority service for managing user authorities.</param>
/// <param name="logger">The logger for logging LDAP login operations.</param> /// <param name="logger">The logger for logging LDAP login operations.</param>
/// <!-- aidoc:v1 sig=f1b8e4e body=6c4718f -->
public LdapLoginService( public LdapLoginService(
IOptions<LdapConfig> ldapConfig, IOptions<LdapConfig> ldapConfig,
IValidator<LdapConfig> validator, IValidator<LdapConfig> validator,
@@ -85,74 +82,83 @@ public class LdapLoginService : ILoginService
/// <returns>The authenticated user.</returns> /// <returns>The authenticated user.</returns>
/// <exception cref="LoginServicesException">Thrown when there is an error during the login process.</exception> /// <exception cref="LoginServicesException">Thrown when there is an error during the login process.</exception>
/// <exception cref="UserNotFoundException">Thrown when the user is not found in the LDAP directory.</exception> /// <exception cref="UserNotFoundException">Thrown when the user is not found in the LDAP directory.</exception>
/// <!-- aidoc:v1 sig=bdfb451 body=e3fc1ac -->
public async Task<User> Login(string username, string password) public async Task<User> Login(string username, string password)
{ {
if (_ldapConfig.Server == null) // Check for LDAP config
throw new LoginServicesException("LDAP Config not found"); if (_ldapConfig.Server == null) throw new LoginServicesException("LDAP Config not found");
var conn = new LdapConnection();
var identifier = new LdapDirectoryIdentifier(_ldapConfig.Server, _ldapConfig.Port ?? 389);
var connection = new LdapConnection(identifier);
try try
{ {
if (_ldapConfig.LdapUser != null) await conn.ConnectAsync(_ldapConfig.Server, _ldapConfig.Port ?? 389);
{
_logger.LogInformation("[LDAP] Using configured LDAP user {LdapUser}", _ldapConfig.LdapUser);
connection.Credential = new NetworkCredential(_ldapConfig.LdapUser, _ldapConfig.LdapPassword);
}
else
{
var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName)
? _ldapConfig.UserDomainName + @"\"
: "") + username;
connection.Credential = new NetworkCredential(ldapUser, password);
}
connection.AuthType = AuthType.Basic;
connection.Bind();
} }
catch (LdapException e) catch (Exception e)
{ {
_logger.LogError("[LDAP] Error binding user {username}", username); _logger.LogError("[LDAP] Error connecting to {server}, port {port}, Exception: {e}", _ldapConfig.Server,
throw new UserNotFoundException(username, e); _ldapConfig.Port, e.Message);
throw;
} }
SearchResultEntry? entry = null; if (_ldapConfig.LdapUser != null)
try
{ {
var request = new SearchRequest( _logger.LogInformation("[LDAP] _ldapConfig.LdapUser is enabled with {LdapUser}", _ldapConfig.LdapUser);
_ldapConfig.SearchBase, await conn.BindAsync(_ldapConfig.LdapUser, _ldapConfig.LdapPassword);
$"({_ldapConfig.UserNameProperty}={username})", }
SearchScope.Subtree else
); {
var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName)
? _ldapConfig.UserDomainName + @"\"
: "") + username;
var response = (SearchResponse)connection.SendRequest(request); try
{
await conn.BindAsync(ldapUser, password);
}
catch (LdapException e)
{
_logger.LogError("[LDAP] Error binding ldapUser: {LdapUser} and password", ldapUser);
foreach (SearchResultEntry current in response.Entries) throw new UserNotFoundException(username, e);
}
}
var results = await conn.SearchAsync(
_ldapConfig.SearchBase,
LdapConnection.ScopeSub,
$"({_ldapConfig.UserNameProperty}={username})",
null,
false);
LdapEntry? entry = null;
while (await results.HasMoreAsync())
{
LdapEntry? current = null;
try
{
current = await results.NextAsync();
}
catch (LdapException ex)
{
_logger.LogWarning("[LDAP] Skipping invalid entry: {error}", ex.Message);
continue;
}
if (current != null)
{ {
entry = current; entry = current;
break; break;
} }
} }
catch (Exception e)
{
_logger.LogError("[LDAP] Search error for user {username}: {error}", username, e.Message);
throw;
}
if (entry == null)
throw new LoginServicesException("LDAP User not found");
if (entry == null) throw new LoginServicesException("LDAP User not found");
var userEntryLdap = GetUser(entry); var userEntryLdap = GetUser(entry);
var user = await GetOrCreateUser(userEntryLdap, entry); var user = await GetOrCreateUser(userEntryLdap, entry);
_logger.LogInformation("[LDAP] entry is {entry} and user {user}", entry, user);
_logger.LogInformation("[LDAP] entry found and user {user}", user); // user.Authorization.AddRange(GetAuthorities(entry));
conn.Disconnect();
connection.Dispose();
return user ?? throw new LoginServicesException("LDAP User not found"); return user ?? throw new LoginServicesException("LDAP User not found");
} }
@@ -162,9 +168,10 @@ public class LdapLoginService : ILoginService
/// <param name="context">The HTTP context of the request.</param> /// <param name="context">The HTTP context of the request.</param>
/// <returns>A task representing the asynchronous operation.</returns> /// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
/// <!-- aidoc:v1 sig=851dc90 -->
public Task<User> Login(HttpContext context) public Task<User> Login(HttpContext context)
=> throw new LoginServicesException("Not implemented"); {
throw new LoginServicesException("Not implemented");
}
/// <summary> /// <summary>
/// This method is not implemented in the LdapLoginService, as the authentication process is handled through the Login(string username, string password) method. /// This method is not implemented in the LdapLoginService, as the authentication process is handled through the Login(string username, string password) method.
@@ -173,9 +180,10 @@ public class LdapLoginService : ILoginService
/// <param name="password">The password of the user to authenticate.</param> /// <param name="password">The password of the user to authenticate.</param>
/// <returns>A task representing the asynchronous operation.</returns> /// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
/// <!-- aidoc:v1 sig=a0b1f46 -->
public Task<User> Authenticate(string username, string password) public Task<User> Authenticate(string username, string password)
=> throw new LoginServicesException("Not implemented"); {
throw new LoginServicesException("Not implemented");
}
/// <summary> /// <summary>
/// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method. /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
@@ -183,9 +191,10 @@ public class LdapLoginService : ILoginService
/// <param name="id">The ID of the user to retrieve.</param> /// <param name="id">The ID of the user to retrieve.</param>
/// <returns>A task representing the asynchronous operation.</returns> /// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
/// <!-- aidoc:v1 sig=877a869 -->
public Task<User?> GetById(ObjectId id) public Task<User?> GetById(ObjectId id)
=> throw new LoginServicesException("Not implemented"); {
throw new LoginServicesException("Not implemented");
}
/// <summary> /// <summary>
/// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method. /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
@@ -193,29 +202,31 @@ public class LdapLoginService : ILoginService
/// <param name="email">The email of the user to retrieve.</param> /// <param name="email">The email of the user to retrieve.</param>
/// <returns>A task representing the asynchronous operation.</returns> /// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
/// <!-- aidoc:v1 sig=ba92b09 -->
public Task<User?> GetByEmail(string email) public Task<User?> GetByEmail(string email)
=> throw new LoginServicesException("Not implemented"); {
throw new LoginServicesException("Not implemented");
}
/// <summary> /// <summary>
/// Retrieves a <see cref="User"/> matching the specified <paramref name="username"/>. /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
/// </summary> /// </summary>
/// <param name="username">The username to look up.</param> /// <param name="username">The username of the user to retrieve.</param>
/// <returns>A <see cref="Task{TResult}"/> that resolves to the <see cref="User"/> if found, or <see langword="null"/> when no matching user exists.</returns> /// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="LoginServicesException">Thrown because the operation is not yet implemented.</exception> /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
/// <!-- aidoc:v1 sig=5184c30 -->
public Task<User?> GetByUsername(string username) public Task<User?> GetByUsername(string username)
=> throw new LoginServicesException("Not implemented"); {
throw new LoginServicesException("Not implemented");
}
/// <summary> /// <summary>
/// Retrieves all <see cref="User"/> entities from the system. This method is not yet implemented and currently throws a <see cref="LoginServicesException"/> when invoked. /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
/// </summary> /// </summary>
/// <returns>A <see cref="Task{List{User}}"/> that represents the asynchronous operation, intended to contain the full collection of <see cref="User"/> entries.</returns> /// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="LoginServicesException">Thrown unconditionally because the method has not been implemented.</exception> /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
/// <!-- aidoc:v1 sig=aa066d6 -->
public Task<List<User>> GetAllUsers() public Task<List<User>> GetAllUsers()
=> throw new LoginServicesException("Not implemented"); {
throw new LoginServicesException("Not implemented");
}
/// <summary> /// <summary>
/// This method retrieves an existing user from the application based on the information obtained from the LDAP entry, or creates a new user if one does not already exist. /// This method retrieves an existing user from the application based on the information obtained from the LDAP entry, or creates a new user if one does not already exist.
@@ -224,53 +235,55 @@ public class LdapLoginService : ILoginService
/// <param name="userEntryLdap">The user information obtained from the LDAP entry.</param> /// <param name="userEntryLdap">The user information obtained from the LDAP entry.</param>
/// <param name="entry">The LDAP entry containing the user's information.</param> /// <param name="entry">The LDAP entry containing the user's information.</param>
/// <returns>The existing or newly created user with updated authorities.</returns> /// <returns>The existing or newly created user with updated authorities.</returns>
/// <!-- aidoc:v1 sig=87de1c2 body=a43bebd -->
private async Task<User?> GetOrCreateUser(User userEntryLdap, LdapEntry entry) private async Task<User?> GetOrCreateUser(User userEntryLdap, LdapEntry entry)
{ {
var userToReturn = (await _userService.Value.GetUserByUserName(userEntryLdap.UserName) ?? var userToReturn = (await _userService.Value.GetUserByUserName(userEntryLdap.UserName) ??
await _userService.Value.GetUserByName(userEntryLdap.Name)) ?? await _userService.Value.GetUserByName(userEntryLdap.Name)) ??
await _userService.Value.CreateUser(userEntryLdap); await _userService.Value.CreateUser(userEntryLdap);
if (userToReturn == null) return userToReturn; if (userToReturn == null) return userToReturn;
userToReturn.Authorization = []; userToReturn.Authorization = [];
var authorities = await CheckAuthorities(userToReturn, entry); var authorities = await CheckAuthorities(userToReturn, entry);
userToReturn.Authorization.AddRange(authorities);
userToReturn.Authorization.AddRange(authorities);
//foreach (var authorization in userToReturn.Authorization)
//{
// Enum.TryParse<RolesType>(authorization.Rol, out var compareRole);
// if (compareRole == RolesType.Admin) userToReturn.Rol = RolesType.Admin;
//}
return userToReturn; return userToReturn;
} }
/// <summary> /// <summary>
/// This method checks the authorities of a user based on the LDAP entry and the application's configuration. /// This method checks the authorities of a user based on the LDAP entry and the application's configuration.
/// </summary> /// </summary>
/// <param name="user">The user whose authorities are being checked.</param> /// <param name="user">The user whose authorities are being checked.</param>
/// <param name="entry">The LDAP entry containing the user's information.</param> /// <param name="entry">The LDAP entry containing the user's information.</param>
/// <returns>A list of updated authorities for the user.</returns> /// <returns>A list of updated authorities for the user.</returns>
/// <!-- aidoc:v1 sig=d2bafd5 body=4af15eb -->
private async Task<List<Authorization>> CheckAuthorities(User user, LdapEntry entry) private async Task<List<Authorization>> CheckAuthorities(User user, LdapEntry entry)
{ {
try try
{ {
var authorizationMap = GetAuthoritiesMap(entry, user); var authorizationMap = GetAuthoritiesMap(entry, user);
var authorizationWhiteList = GetAuthoritiesWhiteList(entry, user); var authorizationWhiteList = GetAuthoritiesWhiteList(entry, user);
_logger.LogInformation(
"[LDAP] user {user} authorizationMap count is {authorizationMap}, authorizationWhiteList count is {authorizationWhiteList}",
user.UserName, authorizationMap.Count, authorizationWhiteList.Count);
// Primero, creamos un HashSet con los DisplayID de la lista blanca para búsqueda eficiente
var whiteListDisplayIds = new HashSet<string?>(authorizationWhiteList.Select(a => a.DisplayId)); var whiteListDisplayIds = new HashSet<string?>(authorizationWhiteList.Select(a => a.DisplayId));
var uniqueMapAuthorizations = authorizationMap // Filtramos los elementos de authorizationMap que no están en la lista blanca, basándonos en DisplayID
.Where(a => !whiteListDisplayIds.Contains(a.DisplayId)); var uniqueMapAuthorizations = authorizationMap.Where(a => !whiteListDisplayIds.Contains(a.DisplayId));
// Finalmente, combinamos los elementos únicos de authorizationMap con los de authorizationWhiteList
var combinedList = authorizationWhiteList.Concat(uniqueMapAuthorizations).ToList(); var combinedList = authorizationWhiteList.Concat(uniqueMapAuthorizations).ToList();
var userAuthorities = await _authorityService.GetUserAuthorities(user.Id); var userAuthorities = await _authorityService.GetUserAuthorities(user.Id);
foreach (var auth in combinedList) foreach (var auth in combinedList)
{ {
var authFound = userAuthorities.Find(c => c.DisplayId == auth.DisplayId); var authFound = userAuthorities.Find(c => c.DisplayId == auth.DisplayId);
if (authFound is { CanUpdate: true }) if (authFound is { CanUpdate: true })
{ {
authFound.Rol = auth.Rol; authFound.Rol = auth.Rol;
@@ -286,41 +299,42 @@ public class LdapLoginService : ILoginService
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("[LDAP] CheckAuthorities error for user {user}: {error}", _logger.LogError("[LDAP] CheckAuthorities for user {user} has exception {ex}", user.UserName, e.Message);
user.UserName, e.Message);
return []; return [];
} }
} }
/// <summary> /// <summary>
/// This method retrieves a list of authorities for a user based on a whitelist defined in the application's configuration. /// This method retrieves a list of authorities for a user based on a whitelist defined in the application's configuration.
/// </summary> /// </summary>
/// <param name="entry">The LDAP entry containing the user's information.</param> /// <param name="entry">The LDAP entry containing the user's information.</param>
/// <param name="user">The user whose authorities are being retrieved.</param> /// <param name="user">The user whose authorities are being retrieved.</param>
/// <returns>A list of authorities for the user based on the whitelist.</returns> /// <returns>A list of authorities for the user based on the whitelist.</returns>
/// <!-- aidoc:v1 sig=a8263f0 body=9e6cc38 -->
private List<Authorization> GetAuthoritiesWhiteList(LdapEntry entry, User user) private List<Authorization> GetAuthoritiesWhiteList(LdapEntry entry, User user)
{ {
try try
{ {
var result = new List<Authorization>(); var userWhiteList = new List<Authorization>();
var userNameProperty = _ldapConfig.UserNameProperty;
var whiteList = _ldapConfig.WhiteList.FindAll(u => var whiteList = _ldapConfig.WhiteList.FindAll(u =>
(u.Name != null && entry.DistinguishedName.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) || (u.Name != null && entry.Dn.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) ||
(u.Username != null && (u.Username != null &&
entry.Attributes[_ldapConfig.UserNameProperty]?[0]?.ToString() userNameProperty != null &&
?.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase) == true) entry.GetAttributeSet().TryGetValue(userNameProperty, out var attr) &&
attr.StringValue != null &&
attr.StringValue.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase))
); );
if (whiteList.Count == 0) return userWhiteList;
foreach (var authorityMap in whiteList) foreach (var authorityMap in whiteList)
{ {
if (!Enum.TryParse<PermissionEnum.RolesType>(authorityMap.Rol, out _)) if (!Enum.TryParse<PermissionEnum.RolesType>(authorityMap.Rol, out _))
continue; continue;
result.Add(new Authorization userWhiteList.Add(new Authorization
{ {
UserId = user.Id, UserId = user.Id,
DisplayId = authorityMap.DisplayId, DisplayId = authorityMap.DisplayId,
@@ -328,59 +342,70 @@ public class LdapLoginService : ILoginService
}); });
} }
return result; return userWhiteList;
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("[LDAP] GetAuthoritiesWhiteList error: {error}", e.Message); _logger.LogError("[LDAP] GetAuthoritiesWhiteList for user {user} has exception {ex}", user.UserName,
e.Message);
return []; return [];
} }
} }
/// <summary> /// <summary>
/// Maps an <see cref="LdapEntry"/> to a <see cref="User"/> object, populating the username from the configured attribute and, when the corresponding properties are configured, composing the user's name from the first and last name attributes. /// This method retrieves a list of authorities for a user based on the LDAP entry and the application's configuration for mapping LDAP groups to authorities.
/// </summary> /// </summary>
/// <param name="ldapEntry">The <see cref="LdapEntry"/> from which the <see cref="User"/> properties are read.</param> /// <param name="ldapEntry">The LDAP entry containing the user's information.</param>
/// <returns>A <see cref="User"/> instance populated from the LDAP entry attributes.</returns> /// <returns>The existing or newly created user with updated authorities.</returns>
/// <!-- aidoc:v1 sig=947181a body=33ffec8 -->
private User GetUser(LdapEntry ldapEntry) private User GetUser(LdapEntry ldapEntry)
{ {
var user = new User var attributes = ldapEntry.GetAttributeSet();
{
UserName = entry.Attributes[_ldapConfig.UserNameProperty]?[0]?.ToString() ?? ""
};
if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty)) var user = new User();
// UserName
if (!string.IsNullOrWhiteSpace(_ldapConfig.UserNameProperty) &&
attributes.TryGetValue(_ldapConfig.UserNameProperty, out var userAttr) &&
userAttr?.StringValue != null)
{ {
var first = entry.Attributes[_ldapConfig.FirstNameProperty]?[0]?.ToString(); user.UserName = userAttr.StringValue;
if (first != null) }
user.Name = first; else
{
user.UserName = "";
} }
if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty)) _logger.LogInformation("[LDAP] GetUser UserName is {UserName} ", user.UserName);
// First name
if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty) &&
attributes.TryGetValue(_ldapConfig.FirstNameProperty, out var firstNameAttr) &&
firstNameAttr?.StringValue != null)
{ {
var last = entry.Attributes[_ldapConfig.LastNameProperty]?[0]?.ToString(); user.Name = firstNameAttr.StringValue;
if (last != null) }
user.Name = string.IsNullOrEmpty(user.Name)
? last // Last name
: $"{user.Name} {last}"; if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty) &&
attributes.TryGetValue(_ldapConfig.LastNameProperty, out var lastNameAttr) &&
lastNameAttr?.StringValue != null)
{
user.Name = string.IsNullOrEmpty(user.Name)
? lastNameAttr.StringValue
: $"{user.Name} {lastNameAttr.StringValue}";
} }
return user; return user;
} }
/// <summary> /// <summary>
/// This method retrieves a list of authorities for a user based on the LDAP entry and the application's configuration for mapping LDAP groups to authorities. /// This method retrieves a list of authorities for a user based on the LDAP entry and the application's configuration for mapping LDAP groups to authorities.
/// </summary> /// </summary>
/// <param name="ldapEntry">The LDAP entry containing the user's information.</param> /// <param name="ldapEntry">The LDAP entry containing the user's information.</param>
/// <param name="user">The user whose authorities are being retrieved.</param> /// <param name="user">The user whose authorities are being retrieved.</param>
/// <returns>A list of authorities for the user based on the LDAP entry and the application's configuration.</returns> /// <returns>A list of authorities for the user based on the LDAP entry and the application's configuration.</returns>
/// <!-- aidoc:v1 sig=1e1f0f2 body=18015c0 -->
private List<Authorization> GetAuthoritiesMap(LdapEntry ldapEntry, User user) private List<Authorization> GetAuthoritiesMap(LdapEntry ldapEntry, User user)
{ {
try try
{ {
@@ -389,14 +414,16 @@ public class LdapLoginService : ILoginService
if (!_ldapConfig.AuthoritiesMap.Any()) if (!_ldapConfig.AuthoritiesMap.Any())
return authorities; return authorities;
var groupAttr = entry.Attributes[_ldapConfig.GroupsProperty]; var attributes = ldapEntry.GetAttributeSet();
if (groupAttr == null) if (!string.IsNullOrWhiteSpace(_ldapConfig.GroupsProperty) ||
!attributes.TryGetValue(_ldapConfig.GroupsProperty!, out var groupsAttr) ||
groupsAttr?.StringValueArray == null)
{
return authorities; return authorities;
}
var groups = groupAttr.GetValues(typeof(string)) var groups = groupsAttr.StringValueArray.ToList();
.Cast<string>()
.ToList();
foreach (var authorityMap in _ldapConfig.AuthoritiesMap) foreach (var authorityMap in _ldapConfig.AuthoritiesMap)
{ {
@@ -419,8 +446,8 @@ public class LdapLoginService : ILoginService
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("[LDAP] GetAuthoritiesMap error: {error}", e.Message); _logger.LogError("Error getting Authorities Map. Return new empty list. Exception: {e}", e);
return []; return [];
} }
} }
} }
@@ -14,12 +14,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="FluentValidation" Version="12.1.1" /> <PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.DirectoryServices.Protocols" Version="10.0.9" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="4.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -14,11 +14,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+2 -1
View File
@@ -25,8 +25,9 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="8.0.28" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="10.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="Microsoft.QualityTools.Testing.Fakes" Version="18.1.1" />
<PackageReference Include="Mongo2Go" Version="4.1.0" /> <PackageReference Include="Mongo2Go" Version="4.1.0" />
<PackageReference Include="MongoMigrations.Core" Version="4.0.15" /> <PackageReference Include="MongoMigrations.Core" Version="4.0.15" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -18,6 +18,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -15,7 +15,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageReference Include="Serilog" Version="4.3.1" /> <PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="SharpCompress" Version="0.49.1" />
<PackageReference Include="Snappier" Version="1.3.1" />
</ItemGroup> </ItemGroup>
@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+11 -1
View File
@@ -30,11 +30,15 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="8.0.28" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.28" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="10.0.9" />
<PackageReference Include="Microsoft.IdentityModel.Protocols" Version="8.19.1" /> <PackageReference Include="Microsoft.IdentityModel.Protocols" Version="8.19.1" />
<PackageReference Include="Microsoft.OpenApi" Version="3.7.0" />
<PackageReference Include="MongoDB.Driver" Version="3.9.0" /> <PackageReference Include="MongoDB.Driver" Version="3.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageReference Include="Quartz" Version="3.18.1" />
<PackageReference Include="Serilog" Version="4.3.1" /> <PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.Dynamic" Version="1.0.9" /> <PackageReference Include="Serilog.Enrichers.Dynamic" Version="1.0.9" />
@@ -44,6 +48,12 @@
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" /> <PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="10.0.9" />
<PackageReference Include="System.Management" Version="10.0.9" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="WebPush" Version="1.0.13" /> <PackageReference Include="WebPush" Version="1.0.13" />
</ItemGroup> </ItemGroup>
+1 -1
View File
@@ -20,7 +20,7 @@
//"ConnectionString": "mongodb://smartuci:!H12o2020@localhost:27017/?authSource=smartuci", //"ConnectionString": "mongodb://smartuci:!H12o2020@localhost:27017/?authSource=smartuci",
//"ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007", //"ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007",
//"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin",
"DatabaseName": "adas" "DatabaseName": "adasDevNewStandard"
}, },
"DatabaseConfigurationAudit": { "DatabaseConfigurationAudit": {
"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin",