refactor: address code review findings across all projects
Apply comprehensive fixes from code reviews including: - Extract shared utilities (SqlFormatHelper, CellValueConverter, DbDestinationBase) - Add interface abstractions (IAuthenticationService, IDatabaseMigrator, IMisQueryBuilder) - Implement SecureStore for encrypted secrets storage - Fix error handling with proper HTTP status codes and logging - Optimize double enumeration in DevEtlRegistry - Add DataSync.Dev README for developer onboarding - Extract filter panel base classes to reduce duplication - Update code review docs to mark all issues as fixed
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
using JdeScoping.Core.Models;
|
||||
|
||||
namespace JdeScoping.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication service interface
|
||||
/// </summary>
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticates a user with the given credentials
|
||||
/// </summary>
|
||||
/// <param name="username">Username</param>
|
||||
/// <param name="password">Password</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>Authentication result</returns>
|
||||
Task<AuthResult> AuthenticateAsync(
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets user information for the given username
|
||||
/// </summary>
|
||||
/// <param name="username">Username to lookup</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>User info if found, null otherwise</returns>
|
||||
Task<UserInfo?> GetUserInfoAsync(
|
||||
string username,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a user is a member of a specific group
|
||||
/// </summary>
|
||||
/// <param name="username">Username to check</param>
|
||||
/// <param name="groupName">Group name or DN to check membership</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>True if user is in the group, false otherwise</returns>
|
||||
Task<bool> IsInGroupAsync(
|
||||
string username,
|
||||
string groupName,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using JdeScoping.Core.Models;
|
||||
|
||||
namespace JdeScoping.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Core authentication service interface.
|
||||
/// Provides credential-based user authentication.
|
||||
/// </summary>
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticates a user with the given credentials.
|
||||
/// </summary>
|
||||
/// <param name="username">Username</param>
|
||||
/// <param name="password">Password</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>Authentication result containing success status and user info if successful</returns>
|
||||
Task<AuthResult> AuthenticateAsync(
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using JdeScoping.Core.Models.SearchResults;
|
||||
|
||||
namespace JdeScoping.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
@@ -11,5 +13,5 @@ public interface IExcelExportService
|
||||
/// <param name="search">Search model with criteria and results.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Excel file as byte array.</returns>
|
||||
Task<byte[]> GenerateAsync(object search, CancellationToken cancellationToken = default);
|
||||
Task<byte[]> GenerateAsync(SearchModel search, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace JdeScoping.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for securely storing and retrieving encrypted secrets.
|
||||
/// </summary>
|
||||
public interface ISecureStoreService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a secret value by key, returning null if not found.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <returns>The secret value, or null if not found.</returns>
|
||||
string? Get(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a required secret value by key, throwing if not found.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <returns>The secret value.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown when the key is not found.</exception>
|
||||
string GetRequired(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a secret value.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <param name="value">The secret value.</param>
|
||||
void Set(string key, string value);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a secret exists.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <returns>True if the secret exists, false otherwise.</returns>
|
||||
bool Contains(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a secret by key.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <returns>True if the secret was removed, false if it didn't exist.</returns>
|
||||
bool Remove(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Persists any pending changes to the store.
|
||||
/// </summary>
|
||||
void Save();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all secret keys in the store.
|
||||
/// </summary>
|
||||
IEnumerable<string> Keys { get; }
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.271" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.Core.Models.Search;
|
||||
|
||||
@@ -58,17 +59,8 @@ public class Search
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(CriteriaJson))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<SearchCriteria>(CriteriaJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TryGetCriteria(out var criteria);
|
||||
return criteria;
|
||||
}
|
||||
set
|
||||
{
|
||||
@@ -78,6 +70,31 @@ public class Search
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to deserialize the search criteria from JSON.
|
||||
/// </summary>
|
||||
/// <param name="criteria">The deserialized criteria, or null if deserialization fails or JSON is empty.</param>
|
||||
/// <param name="logger">Optional logger for warning on deserialization failures.</param>
|
||||
/// <returns>True if deserialization succeeded or JSON was empty; false if deserialization failed.</returns>
|
||||
public bool TryGetCriteria(out SearchCriteria? criteria, ILogger? logger = null)
|
||||
{
|
||||
criteria = null;
|
||||
|
||||
if (string.IsNullOrEmpty(CriteriaJson))
|
||||
return true; // Empty is valid, not an error
|
||||
|
||||
try
|
||||
{
|
||||
criteria = JsonSerializer.Deserialize<SearchCriteria>(CriteriaJson);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "Failed to deserialize search criteria for Search ID {SearchId}", Id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel search results file (binary)
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace JdeScoping.Core.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the secure secrets store.
|
||||
/// </summary>
|
||||
public class SecureStoreOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name in appsettings.json.
|
||||
/// </summary>
|
||||
public const string SectionName = "SecureStore";
|
||||
|
||||
/// <summary>
|
||||
/// Path to the encrypted secrets store file.
|
||||
/// Defaults to "data/secrets.json" relative to app directory.
|
||||
/// </summary>
|
||||
public string StorePath { get; set; } = "data/secrets.json";
|
||||
|
||||
/// <summary>
|
||||
/// Path to the key file (used in development).
|
||||
/// Defaults to "data/secrets.key" relative to app directory.
|
||||
/// </summary>
|
||||
public string KeyFilePath { get; set; } = "data/secrets.key";
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable name containing the master key (used in production).
|
||||
/// If set and the env var exists, it takes precedence over the key file.
|
||||
/// </summary>
|
||||
public string MasterKeyEnvVar { get; set; } = "SCOPINGTOOL_MASTER_KEY";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to auto-create the store and generate a key file on first run.
|
||||
/// </summary>
|
||||
public bool AutoCreateStore { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to migrate existing secrets (RSA key, Excel passwords) on startup.
|
||||
/// </summary>
|
||||
public bool MigrateExistingSecrets { get; set; } = true;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
namespace JdeScoping.Core.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for component lot filter.
|
||||
/// View model for component lot filter with value-based equality semantics.
|
||||
/// Implements <see cref="Equals"/> and <see cref="GetHashCode"/> to support
|
||||
/// deduplication in collections, LINQ Distinct(), and Contains() checks.
|
||||
/// For simple display scenarios without collection operations, use <see cref="LotViewModel"/>.
|
||||
/// </summary>
|
||||
public class ComponentLotViewModel
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
namespace JdeScoping.Core.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for lot projection
|
||||
/// View model for simple lot projection without equality semantics.
|
||||
/// Use this for read-only display and data transfer where collection membership
|
||||
/// checks are not needed. For collection operations requiring deduplication or
|
||||
/// Contains() checks, use <see cref="ComponentLotViewModel"/> instead.
|
||||
/// </summary>
|
||||
public class LotViewModel
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace JdeScoping.Core.ViewModels;
|
||||
/// </summary>
|
||||
public class OperatorViewModel
|
||||
{
|
||||
public int AddressNumber { get; set; }
|
||||
public long AddressNumber { get; set; }
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -62,9 +62,10 @@ public class SearchViewModel
|
||||
/// Constructor that copies values from a Search entity
|
||||
/// </summary>
|
||||
/// <param name="search">Search to copy values from</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when search is null</exception>
|
||||
public SearchViewModel(Search search)
|
||||
{
|
||||
if (search == null) return;
|
||||
ArgumentNullException.ThrowIfNull(search);
|
||||
|
||||
Id = search.Id;
|
||||
UserName = search.UserName;
|
||||
|
||||
Reference in New Issue
Block a user