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:
Joseph Doherty
2026-01-19 11:05:36 -05:00
parent 08f5aa1447
commit 604bfe919c
148 changed files with 8696 additions and 1538 deletions
@@ -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" />
+28 -11
View File
@@ -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;