6 Commits

Author SHA1 Message Date
n8n IEC 62304 Bot 3c2d522c45 docs(iec62304): [REL-1.0.2] apply curated XML doc review updates 2026-07-06 19:14:03 +02:00
n8n IEC 62304 Bot c9446ed5d7 docs(iec62304): [REL-1.0.2] validate and curate XML doc review markers 2026-07-06 19:09:40 +02:00
n8n IEC 62304 Bot 5b4dc2710b docs(iec62304): [REL-1.0.2] apply curated XML doc review updates 2026-07-06 18:42:11 +02:00
n8n IEC 62304 Bot a67a9f5443 docs(iec62304): [REL-1.0.2] validate and curate XML doc review markers 2026-07-06 18:35:52 +02:00
jrojas 8e55c4a7c9 Conflicto de fusión en adas-core.LdapLogin/LdapLoginService.cs 2026-07-06 18:07:25 +02:00
jrojas 6066f55780 se agrego rc102 2026-07-06 18:03:18 +02:00
15 changed files with 351 additions and 335 deletions
@@ -21,23 +21,20 @@
<PackageReference Include="AutoMapper" Version="16.1.1" /> <PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" /> <PrivateAssets>all</PrivateAssets>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" /> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" /> </PackageReference>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.18.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Http" 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="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="2.13.17" /> <PackageReference Include="StackExchange.Redis" Version="3.0.0" />
<PackageReference Include="System.Text.Json" Version="10.0.8" />
</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>
@@ -9,12 +9,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.10" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.27" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.11" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.11" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.8" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.18.0" /> <PrivateAssets>all</PrivateAssets>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" /> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" 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="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+3 -7
View File
@@ -10,17 +10,13 @@
<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.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.28" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.27" />
<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.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
<PackageReference Include="Snappier" Version="1.3.1" /> <PackageReference Include="System.Text.Json" Version="10.0.9" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
<PackageReference Include="System.Text.Json" Version="10.0.8" />
<PackageReference Include="TagLibSharp" Version="2.3.0" /> <PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup> </ItemGroup>
@@ -20,6 +20,7 @@ namespace adas_core.Infrastructure.Repositories;
/// options lists, diagnoses, allergies, procedures, treatments, and other reference data. /// options lists, diagnoses, allergies, procedures, treatments, and other reference data.
/// </summary> /// </summary>
/// <typeparam name="T">The type of MasterList entity to manage.</typeparam> /// <typeparam name="T">The type of MasterList entity to manage.</typeparam>
/// <!-- aidoc:v1 sig=f958de2 -->
public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository<T> where T : MasterList public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository<T> where T : MasterList
{ {
private readonly ApiSettings _apiSettings; private readonly ApiSettings _apiSettings;
@@ -30,6 +31,7 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// <param name="apiSettings">API settings containing collection names configuration.</param> /// <param name="apiSettings">API settings containing collection names configuration.</param>
/// <param name="database">The MongoDB database instance.</param> /// <param name="database">The MongoDB database instance.</param>
/// <exception cref="ArgumentNullException">Thrown when apiSettings is null.</exception> /// <exception cref="ArgumentNullException">Thrown when apiSettings is null.</exception>
/// <!-- aidoc:v1 sig=800d538 body=d2b18a3 -->
public MasterListRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database) public MasterListRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
{ {
if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
@@ -41,6 +43,7 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// Maps different MasterList subtypes to their corresponding MongoDB collection names. /// Maps different MasterList subtypes to their corresponding MongoDB collection names.
/// </summary> /// </summary>
/// <returns>The collection name for the current MasterList type.</returns> /// <returns>The collection name for the current MasterList type.</returns>
/// <!-- aidoc:v1 sig=94e22ff body=64e6c55 -->
public override string GetCollectionName() public override string GetCollectionName()
{ {
return typeof(T) switch return typeof(T) switch
@@ -76,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
@@ -94,6 +98,7 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the entity to delete.</param> /// <param name="id">The ObjectId of the entity to delete.</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=3de1ad6 body=fe0ba8c -->
public async Task Delete(ObjectId id) public async Task Delete(ObjectId id)
{ {
try try
@@ -109,10 +114,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates an existing master list entity with full replacement. /// Replaces the document matching <paramref name="entity"/>'s identifier, or inserts it as a new document when no match is found (upsert).
/// Any exception raised during the operation is logged and rethrown to the caller.
/// </summary> /// </summary>
/// <param name="entity">The entity with updated values.</param> /// <param name="entity">The entity to persist; its <c>Id</c> property is used to locate the existing document to replace.</param>
/// <exception cref="Exception">Throws and re-throws exceptions after logging.</exception> /// <!-- aidoc:v1 sig=019b691 body=863650a -->
public async Task Update(T entity) public async Task Update(T entity)
{ {
try try
@@ -133,6 +139,7 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The ObjectId of the master list.</param>
/// <param name="newOpt">The OptionList with updated values.</param> /// <param name="newOpt">The OptionList with updated values.</param>
/// <returns>The updated OptionList if found; otherwise, null.</returns> /// <returns>The updated OptionList if found; otherwise, null.</returns>
/// <!-- aidoc:v1 sig=64ca4dd body=a7714e6 -->
public async Task<OptionList?> UpdateFullMasterListOption(ObjectId id, OptionList newOpt) public async Task<OptionList?> UpdateFullMasterListOption(ObjectId id, OptionList newOpt)
{ {
var filter = Builders<T>.Filter.Where(o => o.Id == id && o.Options.Any(opt => opt.Id == newOpt.Id) var filter = Builders<T>.Filter.Where(o => o.Id == id && o.Options.Any(opt => opt.Id == newOpt.Id)
@@ -161,11 +168,14 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Finds a master list entity by its ID with options projection limited to 100 items. /// Asynchronously retrieves an entity of type <typeparamref name="T"/> matching the supplied <see cref="ObjectId"/>,
/// applying a projection that limits the <c>options</c> field to 100 elements.
/// Exceptions raised during the lookup are logged and surfaced as a <c>null</c> result.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the entity to retrieve.</param> /// <typeparam name="T">The document type stored in the collection, expected to expose an <see cref="ObjectId"/> <c>Id</c> property.</typeparam>
/// <returns>The MasterList entity if found; otherwise, null.</returns> /// <param name="id">The <see cref="ObjectId"/> that uniquely identifies the entity to find.</param>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <returns>A <see cref="Task{T}"/> containing the matching entity, or <c>null</c> when no document is found or the operation fails.</returns>
/// <!-- aidoc:v1 sig=81ac96a body=1519944 -->
public async Task<T?> FindById(ObjectId id) public async Task<T?> FindById(ObjectId id)
{ {
try try
@@ -189,14 +199,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Finds a specific option within a master list by master and option IDs with locale translation. /// Retrieves a single <see cref="OptionList"/> item identified by <paramref name="masterId"/> and <paramref name="optionId"/>, applying locale-based translation. When <paramref name="locale"/> is <see cref="LocaleEnum.Default"/> or matches the document's default locale, the base option name is used; otherwise the method looks up a translation in the option's locale items and falls back to the base name when no translation exists. Returns <c>null</c> when no matching option is found or when an error is encountered.
/// Uses MongoDB aggregation to apply translations and return the translated option.
/// </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 collection.</param>
/// <param name="optionId">The ObjectId of the option to retrieve.</param> /// <param name="optionId">The identifier of the specific option to retrieve.</param>
/// <param name="locale">The locale for translation.</param> /// <param name="locale">The desired locale used to select translated content.</param>
/// <returns>The OptionList with translated fields if found; otherwise, null.</returns> /// <returns>A <see cref="Task"/> that yields the matching <see cref="OptionList"/>, or <c>null</c> if the option does not exist or the lookup fails.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- 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)
{ {
try try
@@ -336,12 +345,12 @@ 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. /// Retrieves a single <see cref="OptionList"/> entry from the master document that matches the given <paramref name="masterId"/> and <paramref name="optionId"/>. Returns <c>null</c> when no matching document is found, no option matches, or when the operation fails.
/// </summary> /// </summary>
/// <param name="masterId">The ObjectId of the master list.</param> /// <param name="masterId">The <see cref="MongoDB.Bson.ObjectId"/> 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 <see cref="MongoDB.Bson.ObjectId"/> of the specific option to locate within the master's options array.</param>
/// <returns>The OptionList if found; otherwise, null.</returns> /// <returns>A <see cref="Task{TResult}"/> that resolves to the matching <see cref="OptionList"/>, or <c>null</c> if no match is found or an error occurs.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- aidoc:v1 sig=ccc34cb body=71837cc -->
public async Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId) public async Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId)
{ {
try try
@@ -367,13 +376,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Finds a master list entity by its ID with optional locale translation for options. /// Retrieves an entity of type <c>T</c> by its <see cref="ObjectId"/> identifier, applying locale-aware resolution of option names through a MongoDB aggregation pipeline. When <paramref name="locale"/> is <see langword="null"/>, equals <see cref="LocaleEnum.Default"/>, or matches the document's <c>defaultLocale</c>, the original option name is returned; otherwise, the translated name from <c>localeItems</c> is used with a fallback to the original name when no translation exists. Any exception is logged and swallowed, returning <see langword="null"/>.
/// Uses MongoDB aggregation to unwind options and apply translations.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the entity to retrieve.</param> /// <param name="id">The <see cref="ObjectId"/> of the document to retrieve.</param>
/// <param name="locale">Optional locale for translated option names.</param> /// <param name="locale">The optional <see cref="LocaleEnum"/> used to resolve translated option names. When omitted or set to <see cref="LocaleEnum.Default"/>, the original names are used.</param>
/// <returns>The MasterList entity with translated options if found; otherwise, null.</returns> /// <returns>A <see cref="Task{T}"/> that yields the matching entity, or <see langword="null"/> when no document is found or an error is logged.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- aidoc:v1 sig=cb4a658 body=f2e88d8 -->
public async Task<T?> FindById(ObjectId id, LocaleEnum? locale) public async Task<T?> FindById(ObjectId id, LocaleEnum? locale)
{ {
try try
@@ -551,11 +559,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Finds a master list entity by its name. /// Asynchronously searches the underlying collection for an entity matching the specified <paramref name="name"/>,
/// projecting the <c>options</c> field to a maximum of 100 elements to limit the payload size.
/// Returns <see langword="null"/> if no entity is found or if an error occurs while querying the collection.
/// </summary> /// </summary>
/// <param name="name">The name of the master list to retrieve.</param> /// <param name="name">The name of the entity to search for. Used to build an equality filter against the <c>Name</c> field.</param>
/// <returns>The MasterList entity if found; otherwise, null.</returns> /// <returns>A <see cref="Task{T}"/> that yields the matching entity of type <c>T</c>, or <see langword="null"/> when no match is found or an error is logged.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- aidoc:v1 sig=80a1541 body=8b5b558 -->
public async Task<T?> FindByName(string name) public async Task<T?> FindByName(string name)
{ {
try try
@@ -584,16 +594,18 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The ObjectId of the master list.</param>
/// <param name="textSearch">Optional text to search within options.</param> /// <param name="textSearch">Optional text to search within options.</param>
/// <returns>A list of matching OptionList items.</returns> /// <returns>A list of matching OptionList items.</returns>
/// <!-- aidoc:v1 sig=569d223 body=bb7f2c6 -->
public async Task<List<OptionList>> GetMasterListByIdAndTextSearchContaining(ObjectId id, string? textSearch) public async Task<List<OptionList>> GetMasterListByIdAndTextSearchContaining(ObjectId id, string? textSearch)
{ {
return await GetOptionsByTextSearch(textSearch, id); return await GetOptionsByTextSearch(textSearch, id);
} }
/// <summary> /// <summary>
/// Retrieves paginated master lists with optional text filtering. /// Retrieves a paginated master list of <typeparamref name="T"/> items sorted by name in ascending order, applying an optional case-insensitive regex filter on the Name property when a text filter is provided. If no filter request is specified, returns all items without additional filters.
/// </summary> /// </summary>
/// <param name="filter">The pagination and filtering parameters.</param> /// <param name="filter">The <see cref="PaginationFilter"/> containing the pagination and optional text filtering criteria.</param>
/// <returns>A fluent queryable for MasterList results.</returns> /// <returns>An <see cref="IFindFluent{T, T}"/> representing the queryable collection of matching items, sorted by name.</returns>
/// <!-- aidoc:v1 sig=bbb39c7 body=f42ebaa -->
public IFindFluent<T, T> GetPaginatedMasterList(PaginationFilter filter) public IFindFluent<T, T> GetPaginatedMasterList(PaginationFilter filter)
{ {
var filterBuilder = Builders<T>.Filter; var filterBuilder = Builders<T>.Filter;
@@ -616,11 +628,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves paginated options within a master list with optional text filtering. /// Asynchronously retrieves options from the master list identified by <paramref name="listId"/>, optionally applying a case-insensitive text filter against each option's name, description, and option type.
/// Returns an empty list when no master list matches the supplied identifier.
/// </summary> /// </summary>
/// <param name="filter">The pagination and filtering parameters.</param> /// <param name="filter">A <see cref="PaginationFilter"/> whose <c>FilteredRequest.Text</c>, when provided, is used to narrow the options by matching against the name, description, or option type.</param>
/// <param name="listId">The ObjectId of the master list.</param> /// <param name="listId">The identifier of the master list whose options should be returned.</param>
/// <returns>A list of filtered OptionList items.</returns> /// <returns>A task that resolves to a <see cref="List{OptionList}"/> containing the matching options, or an empty list if the master list is not found.</returns>
/// <!-- 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)
{ {
//TODO: LOCALE //TODO: LOCALE
@@ -640,12 +654,14 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Adds a new option to a master list. /// Adds a new <see cref="OptionList"/> to the master list identified by <paramref name="id"/>.
/// Returns null if a matching option already exists, if the update modifies no documents, or if an exception is thrown.
/// When the request comes from the admin panel, the locale items from <paramref name="opt"/> are reused; otherwise, a new locale entry is generated based on the current default locale of the list.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The <see cref="ObjectId"/> of the master list to update.</param>
/// <param name="opt">The option element to add.</param> /// <param name="opt">The <see cref="FilterOptionListElement"/> describing the option to add.</param>
/// <returns>The newly created OptionList if successful; otherwise, null if duplicate exists.</returns> /// <returns>A <see cref="Task{T}"/> containing the created <see cref="OptionList"/> on success, or null if the operation does not persist any change.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- aidoc:v1 sig=1602f0d body=e7fd138 -->
public async Task<OptionList?> AddOptionToMasterList(ObjectId id, FilterOptionListElement opt) public async Task<OptionList?> AddOptionToMasterList(ObjectId id, FilterOptionListElement opt)
{ {
var exist = await GetMasterListByIdAndSearchOptions(id, opt); var exist = await GetMasterListByIdAndSearchOptions(id, opt);
@@ -685,10 +701,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves all master list entities. /// Retrieves all entities of type <typeparamref name="T"/> from the data store asynchronously.
/// If an error occurs during retrieval, it is logged and an empty collection is returned instead of propagating the exception.
/// </summary> /// </summary>
/// <returns>An enumerable of all MasterList entities.</returns> /// <returns>A task that yields an <see cref="IEnumerable{T}"/> containing all entities, or an empty collection when an error is encountered.</returns>
/// <exception cref="Exception">Logs errors and returns empty list on failure.</exception> /// <!-- aidoc:v1 sig=3a61c61 body=40d7129 -->
public async Task<IEnumerable<T>> GetAll() public async Task<IEnumerable<T>> GetAll()
{ {
try try
@@ -704,10 +721,10 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Retrieves all master list entities without options, returning only metadata. /// Retrieves every entry from the master list collection and projects each into a <see cref="MasterListDto"/>, where <see cref="MasterListDto.Options"/> contains the total count of options. If the underlying query fails, the exception is logged and an empty collection is returned.
/// </summary> /// </summary>
/// <returns>An enumerable of MasterListDto containing id, name, description, listType, and options count.</returns> /// <returns>A task that resolves to an <see cref="IEnumerable{MasterListDto}"/> containing all master list entries, or an empty collection when an error occurs.</returns>
/// <exception cref="Exception">Logs errors and returns empty list on failure.</exception> /// <!-- aidoc:v1 sig=e493435 body=1c299ba -->
public async Task<IEnumerable<MasterListDto>> GetAllWithoutOptions() public async Task<IEnumerable<MasterListDto>> GetAllWithoutOptions()
{ {
try try
@@ -732,10 +749,11 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Counts the total number of master list entities in the collection. /// Asynchronously counts the total number of documents in the underlying collection.
/// If the operation fails, the exception is logged and the method returns 0 as a fallback value.
/// </summary> /// </summary>
/// <returns>The total count of entities.</returns> /// <returns>A <see cref="Task{Int32}"/> representing the asynchronous operation, containing the total document count, or 0 if an error occurred.</returns>
/// <exception cref="Exception">Logs errors and returns 0 on failure.</exception> /// <!-- aidoc:v1 sig=f1f0a98 body=01cd3d1 -->
public async Task<int> Count() public async Task<int> Count()
{ {
try try
@@ -751,13 +769,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Searches for options within a master list using multiple filter criteria. /// Retrieves a filtered, locale-aware list of <see cref="OptionList"/> options from the master list document identified by <paramref name="id"/>, applying the optional search criteria supplied in <paramref name="filters"/>. When <paramref name="filters"/> is null or an error occurs during execution, an empty list is returned; translations are resolved per option for the requested locale, falling back to the original name and description when no localized value is available.
/// Uses MongoDB aggregation pipeline to apply filters and locale translations.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The <see cref="ObjectId"/> of the master list document to query.</param>
/// <param name="filters">The filter criteria including text, name, description, and optionType.</param> /// <param name="filters">The optional <see cref="FilterOptionListElement"/> containing the search criteria (option type, text, name, description, and locale) used to filter the resulting options.</param>
/// <returns>A list of matching OptionList items ordered by name.</returns> /// <returns>A <see cref="Task{List{OptionList}}"/> containing the matching options ordered by name, or an empty list when no options match, the filters are null, or the operation fails.</returns>
/// <exception cref="Exception">Logs errors and returns empty list on failure.</exception> /// <!-- 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)
{ {
try try
@@ -966,13 +983,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates a specific option within a master list with locale-aware field updates. /// Updates an option of the master list identified by <paramref name="id"/>, applying locale-aware handling so that <see cref="OptionList.Name"/> is written to the default field when <paramref name="locale"/> equals the master default locale and to the per-locale translation entry otherwise, and persisting the remaining presentation fields (icons, colors, description, dates, <see cref="OptionList.OptionType"/>) only when they are supplied and, where applicable, allowed by the master configuration. Returns the existing duplicate when a name collision is found, the updated <see cref="OptionList"/> on success, or null when no field qualifies, no document is modified, or the operation throws.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">Identifier of the master list that owns the option being updated.</param>
/// <param name="newOpt">The OptionList with updated values.</param> /// <param name="newOpt">Option carrying the new values; only non-null members are applied, and <see cref="OptionList.Name"/> and <see cref="OptionList.OptionType"/> are additionally gated by the required flags defined on the master configuration.</param>
/// <param name="locale">The locale for translation updates.</param> /// <param name="locale">Target <see cref="LocaleEnum"/> used to decide whether to update the default <see cref="OptionList.Name"/> field or the locale-specific translation entry.</param>
/// <returns>The updated OptionList if successful; otherwise, null.</returns> /// <returns>A <see cref="Task{OptionList}"/> resolving to the existing or updated <see cref="OptionList"/>, or null when no update is performed or the caught error is logged.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- 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)
{ {
// 1. Evitar duplicados // 1. Evitar duplicados
@@ -1067,12 +1084,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates a specific option within a master list with full replacement. /// Updates an option within the master list identified by <paramref name="id"/>, replacing it with <paramref name="newOpt"/>.
/// Returns <see langword="null"/> if the master list cannot be found, if no document is modified, or if an exception occurs during the update (which is logged).
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The identifier of the master list that contains the option to update.</param>
/// <param name="newOpt">The OptionList with updated values.</param> /// <param name="newOpt">The new <see cref="OptionList"/> that will replace the existing option in the master list.</param>
/// <returns>The updated OptionList if successful; otherwise, null.</returns> /// <returns>The updated <see cref="OptionList"/> retrieved after a successful update, or <see langword="null"/> if the master list is missing, the update had no effect, or an error was raised.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- aidoc:v1 sig=6def54c body=9554543 -->
public async Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt) public async Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt)
{ {
var master = await FindById(id); var master = await FindById(id);
@@ -1108,6 +1126,7 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The ObjectId of the master list.</param>
/// <param name="deleteOptId">The ObjectId of the option to delete.</param> /// <param name="deleteOptId">The ObjectId of the option to delete.</param>
/// <returns>True if the option was deleted; otherwise, false.</returns> /// <returns>True if the option was deleted; otherwise, false.</returns>
/// <!-- aidoc:v1 sig=e663f16 body=75627bc -->
public async Task<bool> DeleteMasterListOption(ObjectId id, ObjectId deleteOptId) public async Task<bool> DeleteMasterListOption(ObjectId id, ObjectId deleteOptId)
{ {
// Define el filtro para encontrar el documento por su _id // Define el filtro para encontrar el documento por su _id
@@ -1126,12 +1145,15 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates the metadata details for a master list. /// Updates the option details of an existing master list document identified by <paramref name="id"/>, applying only the
/// fields that are provided in <paramref name="opt"/> (<c>CanAddElement</c> and <c>OptionListDetails</c>) using a partial
/// <c>$set</c> update. Returns the supplied <see cref="UpdateMasterListDetailsDto"/> when the document is found and modified,
/// or <see langword="null"/> when no document matches the filter, no fields are modified, or the operation fails.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The <see cref="MongoDB.Bson.ObjectId"/> identifier of the master list document to update.</param>
/// <param name="opt">The UpdateMasterListDetailsDto with updated values.</param> /// <param name="opt">The <see cref="UpdateMasterListDetailsDto"/> containing the new values; only non-null properties are applied to the document.</param>
/// <returns>The updated UpdateMasterListDetailsDto if successful; otherwise, null.</returns> /// <returns>A <see cref="Task{UpdateMasterListDetailsDto}"/> that resolves to the updated DTO on success, or <see langword="null"/> if the document was not found, was not modified, or an error was logged.</returns>
/// <exception cref="Exception">Logs errors and returns null on failure.</exception> /// <!-- aidoc:v1 sig=53d4179 body=49f50ca -->
public async Task<UpdateMasterListDetailsDto?> UpdateOptionDetailsToMasterList(ObjectId id, public async Task<UpdateMasterListDetailsDto?> UpdateOptionDetailsToMasterList(ObjectId id,
UpdateMasterListDetailsDto opt) UpdateMasterListDetailsDto opt)
{ {
@@ -1161,12 +1183,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates the name of a master list. /// Updates the <see cref="Name"/> of the master list entry identified by <paramref name="id"/>.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The identifier of the master list entry to update.</param>
/// <param name="name">The new name.</param> /// <param name="name">The new name to assign to the master list entry.</param>
/// <returns>True if the update was successful; otherwise, false.</returns> /// <returns><see langword="true"/> if the document was modified; otherwise, <see langword="false"/>, including when the update fails due to an exception.</returns>
/// <exception cref="Exception">Logs errors and returns false on failure.</exception> /// <!-- aidoc:v1 sig=5b79a51 body=36dfe99 -->
public async Task<bool> UpdateMasterListName(ObjectId id, string name) public async Task<bool> UpdateMasterListName(ObjectId id, string name)
{ {
var filter = Builders<T>.Filter.And( var filter = Builders<T>.Filter.And(
@@ -1187,12 +1209,13 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Updates the description of a master list. /// Updates the <c>Description</c> field of the document identified by <paramref name="id"/> in the master list collection.
/// Returns <see langword="true"/> when the update modified at least one document, and <see langword="false"/> when no document was modified or when an error is caught and logged.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The <see cref="ObjectId"/> of the document whose description should be updated.</param>
/// <param name="description">The new description.</param> /// <param name="description">The new description value to set on the document.</param>
/// <returns>True if the update was successful; otherwise, false.</returns> /// <returns>A <see cref="Task{Boolean}"/> that resolves to <see langword="true"/> if the document was modified, otherwise <see langword="false"/>.</returns>
/// <exception cref="Exception">Logs errors and returns false on failure.</exception> /// <!-- aidoc:v1 sig=5a27a55 body=3552f46 -->
public async Task<bool> UpdateMasterListDescription(ObjectId id, string description) public async Task<bool> UpdateMasterListDescription(ObjectId id, string description)
{ {
var filter = Builders<T>.Filter.And( var filter = Builders<T>.Filter.And(
@@ -1213,12 +1236,12 @@ 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 "options" array of the MasterList document identified by <paramref name="id"/>, matching on all of the option's properties (name, option type, icons, colors, default flag, and description). Returns <see langword="true"/> when the document was modified (the option was found and pulled), and <see langword="false"/> when no document was modified or when the update fails and the exception is logged.
/// </summary> /// </summary>
/// <param name="id">The ObjectId of the master list.</param> /// <param name="id">The <see cref="ObjectId"/> of the MasterList document to update.</param>
/// <param name="oldOpt">The OptionList to remove.</param> /// <param name="oldOpt">The <see cref="OptionList"/> instance whose properties define the filter used to pull the matching entry from the "options" 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 an option was successfully removed, otherwise <see langword="false"/>.</returns>
/// <exception cref="Exception">Logs errors and returns false on failure.</exception> /// <!-- aidoc:v1 sig=8de50cd body=d974b9b -->
public async Task<bool> RemoveMasterListOption(ObjectId id, OptionList oldOpt) public async Task<bool> RemoveMasterListOption(ObjectId id, OptionList oldOpt)
{ {
var filter = Builders<T>.Filter.Eq("_id", id); var filter = Builders<T>.Filter.Eq("_id", id);
@@ -1247,9 +1270,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Creates necessary indexes for the MasterList collection. /// Creates MongoDB indexes on the underlying collection when the document type is <see cref="DiagnosisList"/>.
/// Currently creates text indexes for DiagnosisList on options.name, options.description, and options._id. /// Three ascending indexes are created on the <c>options.name</c>, <c>options.description</c>, and <c>options._id</c> fields,
/// configured with <c>Background = true</c>, <c>Unique = false</c>, and Spanish text indexing via <c>LanguageOverride</c> and <c>DefaultLanguage</c>.
/// For any other type <typeparamref name="T"/>, no indexes are created.
/// </summary> /// </summary>
/// <!-- aidoc:v1 sig=4955da2 body=2c8d1f7 -->
public override async Task CreateIndexes() public override async Task CreateIndexes()
{ {
if (typeof(T) == typeof(DiagnosisList)) if (typeof(T) == typeof(DiagnosisList))
@@ -1275,18 +1301,21 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// </summary> /// </summary>
/// <param name="textSearch">Optional text to search within options.</param> /// <param name="textSearch">Optional text to search within options.</param>
/// <returns>A list of matching OptionList items.</returns> /// <returns>A list of matching OptionList items.</returns>
/// <!-- aidoc:v1 sig=484b092 body=48e3496 -->
public async Task<List<OptionList>> GetMasterListByTextSearch(string? textSearch) public async Task<List<OptionList>> GetMasterListByTextSearch(string? textSearch)
{ {
return await GetOptionsByTextSearch(textSearch); return await GetOptionsByTextSearch(textSearch);
} }
/// <summary> /// <summary>
/// Searches for options within a master list using text search with accent-aware regex. /// Asynchronously retrieves a list of <see cref="OptionList"/> entries whose name or description matches the provided search text within the document identified by <paramref name="id"/>.
/// Returns an empty list when <paramref name="textSearch"/> is null, empty, or whitespace, or when an error occurs during execution.
/// Matching is performed case-insensitively using a regex pattern, and the resulting options are ordered by name.
/// </summary> /// </summary>
/// <param name="textSearch">Optional text to search within options.</param> /// <param name="textSearch">The text to search for in the option's name and description. If null, empty, or whitespace, the method returns an empty list.</param>
/// <param name="id">Optional master list ObjectId to filter results.</param> /// <param name="id">The optional <see cref="ObjectId"/> of the document whose options will be filtered. Defaults to <see langword="null"/>.</param>
/// <returns>A list of matching OptionList items ordered by name.</returns> /// <returns>A <see cref="Task"/> that yields a <see cref="List{T}"/> of <see cref="OptionList"/> matching the search criteria, ordered by name, or an empty list if no matches are found or an error occurs.</returns>
/// <exception cref="Exception">Logs errors and returns empty list on failure.</exception> /// <!-- 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)
{ {
try try
@@ -1296,7 +1325,6 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
// Normaliza el texto de búsqueda a minúsculas // Normaliza el texto de búsqueda a minúsculas
var normalizedTextSearch = textSearch.ToLowerInvariant(); var normalizedTextSearch = textSearch.ToLowerInvariant();
// Construye la expresión regular dinámica para tener en cuenta la acentuación en las vocales
var regexPattern = BuildRegexPattern(normalizedTextSearch); var regexPattern = BuildRegexPattern(normalizedTextSearch);
// foreach (var c in normalizedTextSearch) // foreach (var c in normalizedTextSearch)
// { // {
@@ -1360,11 +1388,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Creates a fluent query for paginated results with combined filters. /// Builds a fluent MongoDB find query against the collection, combining the supplied <paramref name="filters"/> with a logical AND when any are present, or using an empty filter that matches every document when the list is empty. The returned query is pre-configured with the given <paramref name="sort"/> ordering.
/// </summary> /// </summary>
/// <param name="filters">List of filter definitions to apply.</param> /// <param name="filters">List of <see cref="FilterDefinition{T}"/> predicates to apply; an empty list results in no filtering.</param>
/// <param name="sort">Sort definition for the query results.</param> /// <param name="sort">The <see cref="SortDefinition{T}"/> ordering applied to the results.</param>
/// <returns>A fluent queryable for T results.</returns> /// <returns>An <see cref="IFindFluent{T, T}"/> representing the configured find operation.</returns>
/// <!-- 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)
{ {
var combinedFilter = filters.Any() var combinedFilter = filters.Any()
@@ -1374,12 +1403,12 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Generates new locale items for a master list option based on a default locale. /// Creates a new <see cref="Locale"/> instance and populates each property whose name matches a <see cref="LocaleEnum"/> value, excluding <see cref="LocaleEnum.Default"/> and the value supplied in <paramref name="localeList"/>, with a new <see cref="LocaleItem"/> whose <see cref="LocaleItem.Name"/> is set to <paramref name="opt"/>.
/// 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 to skip when populating the new <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"/> entry.</param>
/// <returns>A Locale object with translations for all other locales.</returns> /// <returns>A <see cref="Locale"/> with its matching <see cref="LocaleItem"/> properties initialized, except for the omitted locale.</returns>
/// <!-- aidoc:v1 sig=147db59 body=9b63847 -->
private Locale GetNewItemLocale(LocaleEnum localeList, string opt) private Locale GetNewItemLocale(LocaleEnum localeList, string opt)
{ {
var newLocale = new Locale(); var newLocale = new Locale();
@@ -1419,6 +1448,7 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
/// </summary> /// </summary>
/// <param name="input">The input string to build the pattern from.</param> /// <param name="input">The input string to build the pattern from.</param>
/// <returns>A regex-compatible pattern string.</returns> /// <returns>A regex-compatible pattern string.</returns>
/// <!-- aidoc:v1 sig=2992c00 body=ae88daf -->
private static string BuildRegexPattern(string input) private static string BuildRegexPattern(string input)
{ {
var regexPattern = new StringBuilder(); var regexPattern = new StringBuilder();
@@ -1452,14 +1482,15 @@ public class MasterListRepository<T> : MongoRepository<T>, IMasterListRepository
} }
/// <summary> /// <summary>
/// Searches for options within a master list by name with locale translation. /// Retrieves the options of a master list document identified by <paramref name="id"/>, filtering those whose resolved name equals <paramref name="newOptName"/> using the supplied <paramref name="locale"/>.
/// Uses MongoDB aggregation pipeline to apply locale-aware filtering. /// The aggregation resolves each option's name by preferring the translation for <paramref name="locale"/> from the locale-specific items, falling back to the original name when no translation is present, and always using the original name when <paramref name="locale"/> matches the document's default locale.
/// Matching options are returned ordered by name; an empty list is returned when the document is not found, no option matches, or an error is logged.
/// </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 match.</param>
/// <param name="newOptName">The option name to search for.</param> /// <param name="newOptName">The option name to filter by after locale resolution.</param>
/// <param name="locale">The locale for translation.</param> /// <param name="locale">The locale used to select the translated name field.</param>
/// <returns>A list of matching OptionList items ordered by name.</returns> /// <returns>A <see cref="Task"/> containing a <see cref="List{OptionList}"/> of matching options ordered by name, or an empty list when no matches are found or an error is logged.</returns>
/// <exception cref="Exception">Logs errors and returns empty list on failure.</exception> /// <!-- 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)
{ {
@@ -9,11 +9,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="EasyNetQ" Version="8.1.4" /> <PackageReference Include="EasyNetQ" Version="8.1.5" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" /> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="2.13.17" /> <PackageReference Include="StackExchange.Redis" Version="3.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+137 -162
View File
@@ -5,11 +5,12 @@ 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 Novell.Directory.Ldap; using System.DirectoryServices.Protocols;
using System.Net;
using Authorization = adas_core.Domain.Models.MongoModels.Authorization;
namespace adas_core.LdapLogin; namespace adas_core.LdapLogin;
@@ -19,6 +20,7 @@ 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>
@@ -50,6 +52,7 @@ 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,
@@ -82,108 +85,97 @@ 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)
{ {
// Check for LDAP config if (_ldapConfig.Server == null)
if (_ldapConfig.Server == null) throw new LoginServicesException("LDAP Config not found"); 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
{ {
await conn.ConnectAsync(_ldapConfig.Server, _ldapConfig.Port ?? 389); if (_ldapConfig.LdapUser != null)
}
catch (Exception e)
{
_logger.LogError("[LDAP] Error connecting to {server}, port {port}, Exception: {e}", _ldapConfig.Server,
_ldapConfig.Port, e.Message);
throw;
}
if (_ldapConfig.LdapUser != null)
{
_logger.LogInformation("[LDAP] _ldapConfig.LdapUser is enabled with {LdapUser}", _ldapConfig.LdapUser);
await conn.BindAsync(_ldapConfig.LdapUser, _ldapConfig.LdapPassword);
}
else
{
var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName)
? _ldapConfig.UserDomainName + @"\"
: "") + username;
try
{ {
await conn.BindAsync(ldapUser, password); _logger.LogInformation("[LDAP] Using configured LDAP user {LdapUser}", _ldapConfig.LdapUser);
connection.Credential = new NetworkCredential(_ldapConfig.LdapUser, _ldapConfig.LdapPassword);
} }
catch (LdapException e) else
{ {
_logger.LogError("[LDAP] Error binding ldapUser: {LdapUser} and password", ldapUser); var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName)
? _ldapConfig.UserDomainName + @"\"
: "") + username;
throw new UserNotFoundException(username, e); connection.Credential = new NetworkCredential(ldapUser, password);
}
}
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) connection.AuthType = AuthType.Basic;
connection.Bind();
}
catch (LdapException e)
{
_logger.LogError("[LDAP] Error binding user {username}", username);
throw new UserNotFoundException(username, e);
}
SearchResultEntry? entry = null;
try
{
var request = new SearchRequest(
_ldapConfig.SearchBase,
$"({_ldapConfig.UserNameProperty}={username})",
SearchScope.Subtree
);
var response = (SearchResponse)connection.SendRequest(request);
foreach (SearchResultEntry current in response.Entries)
{ {
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);
// user.Authorization.AddRange(GetAuthorities(entry)); _logger.LogInformation("[LDAP] entry found and user {user}", user);
conn.Disconnect();
connection.Dispose();
return user ?? throw new LoginServicesException("LDAP User not found"); return user ?? throw new LoginServicesException("LDAP User not found");
} }
/// <summary> /// <summary>
/// This method is not implemented in the LdapLoginService, as the login process is handled through the Login(string username, string password) method. /// Authenticates a user using the provided HTTP context. The implementation is not yet provided and the method always throws a <see cref="LoginServicesException"/>.
/// </summary> /// </summary>
/// <param name="context">The HTTP context of the request.</param> /// <param name="context">The current <see cref="HttpContext"/> carrying the request data used for authentication.</param>
/// <returns>A task representing the asynchronous operation.</returns> /// <returns>A <see cref="Task{User}"/> that will resolve to the authenticated user once the method is implemented.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown because the login operation has not been 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. /// Authenticates a user with the specified <paramref name="username"/> and <paramref name="password"/> credentials.
/// </summary> /// </summary>
/// <param name="username">The username of the user to authenticate.</param> /// <param name="username">The username of the user to authenticate.</param>
/// <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 <see cref="Task{User}"/> that resolves to the authenticated <see cref="User"/>.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown when the method is invoked, as authentication 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.
@@ -191,42 +183,41 @@ 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. /// Retrieves a <see cref="User"/> identified by the supplied email address.
/// The method is not implemented and always throws a <see cref="LoginServicesException"/>.
/// </summary> /// </summary>
/// <param name="email">The email of the user to retrieve.</param> /// <param name="email">The email address used to look up the user.</param>
/// <returns>A task representing the asynchronous operation.</returns> /// <returns>A task that resolves to the matching <see cref="User"/>, or null if no user is found with the given email.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown because 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>
/// 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. /// Asynchronously retrieves a <see cref="User"/> by the supplied <paramref name="username"/>.
/// The current implementation always throws <see cref="LoginServicesException"/> because the operation is not implemented.
/// </summary> /// </summary>
/// <param name="username">The username of the user to retrieve.</param> /// <param name="username">The username used to look up the <see cref="User"/>.</param>
/// <returns>A task representing the asynchronous operation.</returns> /// <returns>A <see cref="Task{TResult}"/> that resolves to the matching <see cref="User"/>, or <see langword="null"/> if no user is found.</returns>
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception> /// <exception cref="LoginServicesException">Thrown for every invocation because the operation 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>
/// 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.
/// </summary> /// </summary>
/// <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=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.
@@ -235,55 +226,53 @@ 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));
// Filtramos los elementos de authorizationMap que no están en la lista blanca, basándonos en DisplayID var uniqueMapAuthorizations = authorizationMap
var uniqueMapAuthorizations = authorizationMap.Where(a => !whiteListDisplayIds.Contains(a.DisplayId)); .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;
@@ -299,42 +288,41 @@ public class LdapLoginService : ILoginService
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("[LDAP] CheckAuthorities for user {user} has exception {ex}", user.UserName, e.Message); _logger.LogError("[LDAP] CheckAuthorities error for user {user}: {error}",
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 userWhiteList = new List<Authorization>(); var result = new List<Authorization>();
var userNameProperty = _ldapConfig.UserNameProperty;
var whiteList = _ldapConfig.WhiteList.FindAll(u => var whiteList = _ldapConfig.WhiteList.FindAll(u =>
(u.Name != null && entry.Dn.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) || (u.Name != null && entry.DistinguishedName.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) ||
(u.Username != null && (u.Username != null &&
userNameProperty != null && entry.Attributes[_ldapConfig.UserNameProperty]?[0]?.ToString()
entry.GetAttributeSet().TryGetValue(userNameProperty, out var attr) && ?.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase) == true)
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;
userWhiteList.Add(new Authorization result.Add(new Authorization
{ {
UserId = user.Id, UserId = user.Id,
DisplayId = authorityMap.DisplayId, DisplayId = authorityMap.DisplayId,
@@ -342,70 +330,59 @@ public class LdapLoginService : ILoginService
}); });
} }
return userWhiteList; return result;
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("[LDAP] GetAuthoritiesWhiteList for user {user} has exception {ex}", user.UserName, _logger.LogError("[LDAP] GetAuthoritiesWhiteList error: {error}", e.Message);
e.Message);
return []; return [];
} }
} }
/// <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. /// Constructs a <see cref="User"/> from the attributes of the supplied <paramref name="ldapEntry"/>, mapping the LDAP username, first name, and last name properties according to the current configuration. The username falls back to an empty string when the configured attribute is missing, and first/last name values are only applied when their corresponding configuration entries are set and the LDAP entry exposes those attributes, with the last name appended to the first name when both are available.
/// </summary> /// </summary>
/// <param name="ldapEntry">The LDAP entry containing the user's information.</param> /// <param name="ldapEntry">The <see cref="LdapEntry"/> whose attributes are read to populate the <see cref="User"/>.</param>
/// <returns>The existing or newly created user with updated authorities.</returns> /// <returns>A <see cref="User"/> populated from the <paramref name="ldapEntry"/> attributes.</returns>
/// <!-- aidoc:v1 sig=947181a body=33ffec8 -->
private User GetUser(LdapEntry ldapEntry) private User GetUser(LdapEntry ldapEntry)
{ {
var attributes = ldapEntry.GetAttributeSet(); var user = new User
var user = new User();
// UserName
if (!string.IsNullOrWhiteSpace(_ldapConfig.UserNameProperty) &&
attributes.TryGetValue(_ldapConfig.UserNameProperty, out var userAttr) &&
userAttr?.StringValue != null)
{ {
user.UserName = userAttr.StringValue; UserName = entry.Attributes[_ldapConfig.UserNameProperty]?[0]?.ToString() ?? ""
} };
else
if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty))
{ {
user.UserName = ""; var first = entry.Attributes[_ldapConfig.FirstNameProperty]?[0]?.ToString();
if (first != null)
user.Name = first;
} }
_logger.LogInformation("[LDAP] GetUser UserName is {UserName} ", user.UserName); if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty))
// First name
if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty) &&
attributes.TryGetValue(_ldapConfig.FirstNameProperty, out var firstNameAttr) &&
firstNameAttr?.StringValue != null)
{ {
user.Name = firstNameAttr.StringValue; var last = entry.Attributes[_ldapConfig.LastNameProperty]?[0]?.ToString();
} if (last != null)
user.Name = string.IsNullOrEmpty(user.Name)
// Last name ? last
if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty) && : $"{user.Name} {last}";
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
{ {
@@ -414,16 +391,14 @@ public class LdapLoginService : ILoginService
if (!_ldapConfig.AuthoritiesMap.Any()) if (!_ldapConfig.AuthoritiesMap.Any())
return authorities; return authorities;
var attributes = ldapEntry.GetAttributeSet(); var groupAttr = entry.Attributes[_ldapConfig.GroupsProperty];
if (!string.IsNullOrWhiteSpace(_ldapConfig.GroupsProperty) || if (groupAttr == null)
!attributes.TryGetValue(_ldapConfig.GroupsProperty!, out var groupsAttr) ||
groupsAttr?.StringValueArray == null)
{
return authorities; return authorities;
}
var groups = groupsAttr.StringValueArray.ToList(); var groups = groupAttr.GetValues(typeof(string))
.Cast<string>()
.ToList();
foreach (var authorityMap in _ldapConfig.AuthoritiesMap) foreach (var authorityMap in _ldapConfig.AuthoritiesMap)
{ {
@@ -446,8 +421,8 @@ public class LdapLoginService : ILoginService
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error getting Authorities Map. Return new empty list. Exception: {e}", e); _logger.LogError("[LDAP] GetAuthoritiesMap error: {error}", e.Message);
return []; return [];
} }
} }
} }
@@ -14,7 +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="Novell.Directory.Ldap.NETStandard" Version="4.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.DirectoryServices.Protocols" Version="10.0.9" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -8,13 +8,17 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\adas-core.Authentication\adas-core.Authentication.csproj"/> <ProjectReference Include="..\adas-core.Authentication\adas-core.Authentication.csproj" />
<ProjectReference Include="..\adas-core.Infrastructure\adas-core.Infrastructure.csproj"/> <ProjectReference Include="..\adas-core.Infrastructure\adas-core.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0"/> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
</Project> </Project>
+6 -2
View File
@@ -21,14 +21,18 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="8.0.28" />
<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" />
<PackageReference Include="NUnit" Version="4.6.1" /> <PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" /> <PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.13.0"> <PackageReference Include="NUnit.Analyzers" Version="4.14.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>
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -14,6 +14,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -15,11 +15,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
<PackageReference Include="Serilog" Version="4.3.1" /> <PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="SharpCompress" Version="0.49.0" />
<PackageReference Include="Snappier" Version="1.3.1" />
</ItemGroup> </ItemGroup>
@@ -9,7 +9,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+2 -2
View File
@@ -21,7 +21,7 @@ using adas_core.WebSocket;
using adas_core.WebSocket.Hubs; using adas_core.WebSocket.Hubs;
using audit_logs.Extensions; using audit_logs.Extensions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -120,7 +120,7 @@ builder.Services.AddSwaggerGen(c =>
c.MapType<ObjectId>(() => new OpenApiSchema c.MapType<ObjectId>(() => new OpenApiSchema
{ {
Type = "string", Type = JsonSchemaType.String,
Format = "objectid" Format = "objectid"
}); });
}); });
+12 -15
View File
@@ -13,7 +13,7 @@
<Company>Epigram Technologies</Company> <Company>Epigram Technologies</Company>
<Product>ADAS Core</Product> <Product>ADAS Core</Product>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors> <TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
@@ -24,15 +24,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AuditLogs" Version="1.0.59" /> <PackageReference Include="AuditLogs" Version="1.0.59" />
<PackageReference Include="EasyNetQ" Version="8.1.4" /> <PackageReference Include="EasyNetQ" Version="8.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.28" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.3.0" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.27" /> <PrivateAssets>all</PrivateAssets>
<PackageReference Include="Microsoft.IdentityModel.Protocols" Version="8.18.0" /> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="8.0.28" />
<PackageReference Include="Microsoft.IdentityModel.Protocols" Version="8.19.1" />
<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" />
@@ -42,18 +44,13 @@
<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.8" />
<PackageReference Include="System.Management" Version="10.0.8" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<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>
<ItemGroup> <ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="10.2.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.27" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.28" />
</ItemGroup> </ItemGroup>
<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": "adasDevNewStandard" "DatabaseName": "adas"
}, },
"DatabaseConfigurationAudit": { "DatabaseConfigurationAudit": {
"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin",