Files
Joseph Doherty 26ff8d9b4f Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
2026-01-02 07:43:29 -05:00

657 lines
32 KiB
Markdown

# Web API and Authentication Specification
## Purpose
This specification defines the REST API endpoints, SignalR real-time communication hub, and LDAP authentication system for the JDE Scoping Tool. The system provides a single .NET 10 service that exposes REST APIs for Blazor WebAssembly clients to manage searches, perform lookup operations, upload/download files, and receive real-time status updates via SignalR. Authentication is performed against an LDAP directory server with group membership verification, with support for a development-mode bypass.
## Source Reference
| Legacy Files | NEW/ Target | Purpose |
|--------------|-------------|---------|
| OLD/WebInterface/Controllers/SearchController.cs | NEW/ScopingTool.Api/Controllers/SearchController.cs | Search management API - create, view, copy, save searches and download results |
| OLD/WebInterface/Controllers/AccountController.cs | NEW/ScopingTool.Api/Controllers/AuthController.cs | User authentication - login, logout, authorization |
| OLD/WebInterface/Controllers/LookupController.cs | NEW/ScopingTool.Api/Controllers/LookupController.cs | Autocomplete lookup APIs for items, profit centers, work centers, operators |
| OLD/WebInterface/Controllers/FileIOController.cs | NEW/ScopingTool.Api/Controllers/FileController.cs | Excel file upload/download for bulk data import |
| OLD/WebInterface/Controllers/CrudController.cs | NEW/ScopingTool.Api/Controllers/ApiControllerBase.cs | Base controller with current user context |
| OLD/WebInterface/Hubs/StatusHub.cs | NEW/ScopingTool.Api/Hubs/StatusHub.cs | SignalR hub for real-time status and search updates |
| OLD/WebInterface/Helpers/LDAPHelper.cs | NEW/ScopingTool.Api/Services/LdapAuthService.cs | LDAP server authentication and user lookup |
| OLD/WebInterface/Security/UserIdentity.cs | NEW/ScopingTool.Api/Security/UserIdentity.cs | Claims-based user identity from LDAP |
| OLD/WebInterface/Models/LogonRequest.cs | NEW/ScopingTool.Api/Models/LoginRequest.cs | Login request model |
| OLD/DataModel/Models/LDAPEntry.cs | NEW/ScopingTool.Domain/Models/UserInfo.cs | User information model (renamed from LDAPEntry) |
| OLD/DataModel/Models/StatusUpdate.cs | NEW/ScopingTool.Domain/Models/StatusUpdate.cs | Process status update model |
| OLD/DataModel/Models/SearchUpdate.cs | NEW/ScopingTool.Domain/Models/SearchUpdate.cs | Search status update model |
## Requirements
### Requirement: Authentication Service Interface
The system SHALL provide an abstraction for authentication to support LDAP authentication in production and fake authentication in development mode.
#### Interface Definition
```csharp
public interface IAuthService
{
Task<AuthResult> AuthenticateAsync(string username, string password, CancellationToken ct = default);
Task<UserInfo?> GetUserInfoAsync(string username, CancellationToken ct = default);
}
public record AuthResult(bool Success, UserInfo? User, string? ErrorMessage);
```
#### Implementations
- `LdapAuthService` - Production LDAP authentication using `System.DirectoryServices.Protocols`
- `FakeAuthService` - Development mode bypass that accepts any credentials
#### Business Rules
- The system SHALL register `IAuthService` in the DI container based on configuration
- The system SHALL use `LdapAuthService` when `AuthOptions.UseFakeAuth` is false
- The system SHALL use `FakeAuthService` when `AuthOptions.UseFakeAuth` is true (development only)
- `FakeAuthService` SHALL return a predefined `UserInfo` for any username/password combination
#### Scenario: Production mode uses LDAP authentication
- **WHEN** the application starts with `AuthOptions.UseFakeAuth = false`
- **THEN** `LdapAuthService` is registered as `IAuthService` and all authentication flows use LDAP
#### Scenario: Development mode uses fake authentication
- **WHEN** the application starts with `AuthOptions.UseFakeAuth = true`
- **THEN** `FakeAuthService` is registered as `IAuthService` and any credentials are accepted
### Requirement: Configuration Options
The system SHALL use strongly-typed configuration options for LDAP and authentication settings.
#### LdapOptions
```csharp
public class LdapOptions
{
public const string SectionName = "Ldap";
public string[] ServerUrls { get; set; } = Array.Empty<string>();
public string GroupDn { get; set; } = string.Empty;
public string SearchBase { get; set; } = string.Empty;
public int ConnectionTimeoutSeconds { get; set; } = 30;
}
```
#### AuthOptions
```csharp
public class AuthOptions
{
public const string SectionName = "Auth";
public bool UseFakeAuth { get; set; } = false;
public string CookieName { get; set; } = "ScopingTool.Auth";
public int CookieExpirationMinutes { get; set; } = 480; // 8 hours
public string[] AdminBypassUsers { get; set; } = Array.Empty<string>(); // Usernames that bypass group check
public long MaxUploadSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB default
}
```
#### LDAP Connection Management
The system SHALL use a connection-per-request pattern for LDAP connections. Each authentication request creates a new `LdapConnection`, authenticates, and disposes. Connection pooling is NOT used due to LDAP bind credential requirements.
#### Business Rules
- The system SHALL bind `LdapOptions` from the `Ldap` configuration section
- The system SHALL bind `AuthOptions` from the `Auth` configuration section
- The system SHALL use `IOptions<LdapOptions>` and `IOptions<AuthOptions>` for injection
### Requirement: LDAP Authentication
The system SHALL authenticate users against an LDAP directory server and verify group membership before granting access.
#### Inputs
- Username (sAMAccountName format)
- Password (plain text, transmitted over HTTPS)
- LDAP server URLs (from `IOptions<LdapOptions>`, supports multiple URLs for failover)
- LDAP group distinguished name (from `IOptions<LdapOptions>`)
#### Outputs
- `AuthResult` containing:
- Success/failure indicator
- `UserInfo` on success (see Data Models section)
- Error message on failure
#### Business Rules
- The system SHALL use `System.DirectoryServices.Protocols.LdapConnection` for cross-platform compatibility
- The system SHALL attempt authentication against each configured LDAP server URL sequentially until one succeeds
- The system SHALL verify the user is a member of the configured LDAP group after successful bind
- The system SHALL use the sAMAccountName LDAP filter format `(sAMAccountName={0})` for user lookup
- The system SHALL extract user properties: distinguishedName, givenName, sn, mail, title
- The system SHALL compute DisplayName as `"{FirstName} {LastName}".Trim()` or fall back to Username if both are empty
- The system SHALL sign out any existing session before creating a new one
- The system SHALL use non-persistent (session-only) authentication cookies
#### Scenario: Successful LDAP authentication with group membership
- **WHEN** a user submits valid credentials for an LDAP user who is a member of the required group
- **THEN** the system authenticates against the LDAP server, verifies group membership, creates a claims-based identity, signs in the user, and returns a success response with user info
#### Scenario: Failed LDAP authentication with invalid credentials
- **WHEN** a user submits invalid credentials
- **THEN** the system returns `AuthResult { Success = false, ErrorMessage = "Incorrect username or password" }`
#### Scenario: Valid credentials but user not in required group
- **WHEN** a user submits valid credentials but is not a member of the required LDAP group
- **THEN** the system returns `AuthResult { Success = false, ErrorMessage = "User is not a member of the required security group" }`
#### Scenario: LDAP server unavailable with failover
- **WHEN** the primary LDAP server is unavailable but a secondary server is configured
- **THEN** the system attempts authentication against each configured server in sequence until one succeeds or all fail
#### Scenario: All LDAP servers unavailable
- **WHEN** all configured LDAP servers are unavailable
- **THEN** the system returns `AuthResult { Success = false, ErrorMessage = "Unable to connect to directory server" }`
### Requirement: User Session Management
The system SHALL maintain user session state using ASP.NET Core cookie authentication.
#### Inputs
- Authenticated user identity (from LDAP or fake auth)
- Session cookie
#### Outputs
- User context available to all authorized controllers via `HttpContext.User`
- Session termination on logout
#### Business Rules
- The system SHALL use `HttpContext.SignInAsync()` with `CookieAuthenticationDefaults.AuthenticationScheme` for sign-in
- The system SHALL use `HttpContext.SignOutAsync()` for sign-out
- The system SHALL store user claims in a `ClaimsPrincipal` with cookie authentication scheme
- The system SHALL provide access to current user's `UserInfo` through `HttpContext.User` claims
- The system SHALL clear authentication cookies on logout
- The system SHALL return HTTP 401 Unauthorized for unauthenticated API requests (no redirect for Blazor WASM)
#### Scenario: Access protected resource while authenticated
- **WHEN** an authenticated user requests a protected resource
- **THEN** the system provides the current user context and serves the requested resource
#### Scenario: Access protected resource while unauthenticated
- **WHEN** an unauthenticated user requests a protected resource with `[Authorize]` attribute
- **THEN** the system returns HTTP 401 Unauthorized
#### Scenario: User logs out
- **WHEN** an authenticated user requests logout via `POST /api/auth/logout`
- **THEN** the system calls `HttpContext.SignOutAsync()`, clears all authentication cookies, and returns HTTP 200 OK
#### Scenario: Parse user identity from claims
- **WHEN** a controller accesses `HttpContext.User`
- **THEN** the system provides access to user claims including Username, FirstName, LastName, Email, and Title
### Requirement: Auth API Endpoints
The system SHALL provide REST API endpoints for authentication operations.
#### Inputs
| Endpoint | Method | Parameters | Description |
|----------|--------|------------|-------------|
| `/api/auth/login` | POST | `LoginRequest` body | Authenticate user |
| `/api/auth/logout` | POST | - | Sign out current user |
| `/api/auth/me` | GET | - | Get current user info |
#### Outputs
- JSON responses with `UserInfo` on success
- HTTP 401 Unauthorized on authentication failure
- HTTP 200 OK on successful logout
#### Business Rules
- The system SHALL use `[ApiController]` attribute on the controller
- The system SHALL use attribute routing with `/api/auth` prefix
- The system SHALL return `UserInfo` JSON (not redirect) on successful login for Blazor WASM compatibility
- The system SHALL call `IAuthService.AuthenticateAsync()` for login operations
#### Scenario: Successful login
- **WHEN** a user posts valid credentials to `/api/auth/login`
- **THEN** the system authenticates, creates a session, and returns HTTP 200 with `UserInfo` JSON
#### Scenario: Failed login
- **WHEN** a user posts invalid credentials to `/api/auth/login`
- **THEN** the system returns HTTP 401 Unauthorized with error message
#### Scenario: Get current user
- **WHEN** an authenticated user requests `/api/auth/me`
- **THEN** the system returns HTTP 200 with `UserInfo` JSON for the current user
#### Scenario: Get current user when not authenticated
- **WHEN** an unauthenticated user requests `/api/auth/me`
- **THEN** the system returns HTTP 401 Unauthorized
### Requirement: Search API Endpoints
The system SHALL provide REST API endpoints for search management operations.
#### Inputs
| Endpoint | Method | Parameters | Description |
|----------|--------|------------|-------------|
| `/api/search` | GET | - | Get current user's searches |
| `/api/search/queue` | GET | - | Get all queued searches |
| `/api/search/{id}` | GET | id (int) | Get search by ID |
| `/api/search/{id}/copy` | POST | id (int) | Copy search with reset status |
| `/api/search` | POST | `SearchViewModel` body | Create/submit search |
| `/api/search/{id}/results` | GET | id (int) | Download search results Excel file |
#### Outputs
- JSON responses using System.Text.Json serialization
- File download for results (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
- Search ID on successful create
#### Business Rules
- The system SHALL use `[ApiController]` attribute on the controller
- The system SHALL use attribute routing with `/api/search` prefix
- The system SHALL require authorization for all search endpoints via `[Authorize]` attribute
- The system SHALL filter GetSearches to only return searches owned by the current user
- The system SHALL order user searches by StartDT descending (most recent first)
- The system SHALL reset Status, UserName, SubmitDT, StartDT, and EndDT when copying a search
- The system SHALL publish a SearchUpdate to the SignalR hub when a new search is saved
- The system SHALL return the new search ID on successful create
- The system SHALL set filename to "search_results.xlsx" for result downloads
- The system SHALL inject `IHubContext<StatusHub>` via DI for SignalR notifications
#### Scenario: Get user's search history
- **WHEN** an authenticated user requests `GET /api/search`
- **THEN** the system returns a JSON array of `SearchViewModel` objects for searches owned by that user, ordered by most recent first
#### Scenario: Create new search
- **WHEN** an authenticated user posts a `SearchViewModel` to `POST /api/search`
- **THEN** the system converts the view model to a Search entity, submits it to the database, publishes a SignalR notification via `IHubContext<StatusHub>`, and returns HTTP 201 Created with the new search ID
#### Scenario: Copy existing search
- **WHEN** an authenticated user requests `POST /api/search/{id}/copy`
- **THEN** the system loads the original search, resets the status to New, clears timestamps, sets the current user as owner, saves the copy, and returns HTTP 201 Created with the new search ID
#### Scenario: Download search results
- **WHEN** an authenticated user requests `GET /api/search/{id}/results`
- **THEN** the system retrieves the binary Excel data from the database and returns it as a file download with Content-Type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
#### Scenario: Download results for search without results
- **WHEN** an authenticated user requests results for a search that has not completed
- **THEN** the system returns HTTP 404 Not Found
### Requirement: Lookup API Endpoints
The system SHALL provide REST API endpoints for autocomplete/lookup operations.
#### Inputs
| Endpoint | Method | Parameters | Description |
|----------|--------|------------|-------------|
| `/api/lookup/items` | GET | `q` (string) | Search items by number |
| `/api/lookup/profit-centers` | GET | `q` (string) | Search profit centers |
| `/api/lookup/work-centers` | GET | `q` (string) | Search work centers |
| `/api/lookup/operators` | GET | `q` (string) | Search operators/users |
#### Outputs
- JSON arrays of matching entities converted to view models
- Results ordered alphabetically by primary identifier
#### Business Rules
- The system SHALL use `[ApiController]` attribute on the controller
- The system SHALL use attribute routing with `/api/lookup` prefix
- The system SHALL NOT require authorization for lookup endpoints (public access)
- The system SHALL order item results by ItemNumber
- The system SHALL order profit center results by Code
- The system SHALL order work center results by Code
- The system SHALL order operator results by FullName
#### Scenario: Search for items by partial number
- **WHEN** a user requests `GET /api/lookup/items?q=ABC`
- **THEN** the system searches for items matching the partial number and returns matching `ItemViewModel` objects ordered by ItemNumber
#### Scenario: Search for profit centers
- **WHEN** a user requests `GET /api/lookup/profit-centers?q=100`
- **THEN** the system searches for profit centers matching the partial code and returns matching view models ordered by Code
#### Scenario: Search for work centers
- **WHEN** a user requests `GET /api/lookup/work-centers?q=MACH`
- **THEN** the system searches for work centers matching the partial code and returns matching view models ordered by Code
#### Scenario: Search for operators by name
- **WHEN** a user requests `GET /api/lookup/operators?q=Smith`
- **THEN** the system searches for users matching the partial name and returns matching view models ordered by FullName
### Requirement: File API Endpoints
The system SHALL provide REST API endpoints for Excel file upload and download operations using ClosedXML.
#### Inputs
| Endpoint | Method | Parameters | Description |
|----------|--------|------------|-------------|
| `/api/file/work-orders/upload` | POST | `IFormFile file` | Upload work orders from Excel |
| `/api/file/work-orders/template` | POST | `List<long>` body | Generate work order template, returns cache key |
| `/api/file/work-orders/template/{key}` | GET | key (Guid) | Download cached template |
| `/api/file/part-numbers/upload` | POST | `IFormFile file` | Upload part numbers from Excel |
| `/api/file/part-numbers/template` | POST | `List<ItemViewModel>` body | Generate part number template |
| `/api/file/part-numbers/template/{key}` | GET | key (Guid) | Download cached template |
| `/api/file/component-lots/upload` | POST | `IFormFile file` | Upload component lots from Excel |
| `/api/file/component-lots/template` | POST | `List<LotViewModel>` body | Generate component lot template |
| `/api/file/component-lots/template/{key}` | GET | key (Guid) | Download cached template |
| `/api/file/part-operations/upload` | POST | `IFormFile file` | Upload part operations from Excel |
| `/api/file/part-operations/template` | POST | `List<PartOperationViewModel>` body | Generate part operations template |
| `/api/file/part-operations/template/{key}` | GET | key (Guid) | Download cached template |
#### Outputs
- `FileUploadResult<T>` with WasSuccessful, ErrorMessage, and Data properties
- GUID key for cached file downloads
- Excel file stream for GET download requests
#### Business Rules
- The system SHALL use `[ApiController]` attribute on the controller
- The system SHALL use attribute routing with `/api/file` prefix
- The system SHALL NOT require authorization for file endpoints (matches legacy behavior)
- The system SHALL accept file uploads via `IFormFile` parameter
- The system SHALL parse Excel files using ClosedXML library, reading from row 2 (skip header)
- The system SHALL inject `IMemoryCache` via DI for template caching
- The system SHALL cache generated templates with 1-minute absolute expiration
- The system SHALL use GUID keys for cached file retrieval
- The system SHALL return HTTP 404 if cached file not found or expired
- The system SHALL remove cached data after successful download
- The system SHALL deduplicate uploaded data using DistinctBy before returning (except part operations)
#### Scenario: Upload work orders from Excel
- **WHEN** a user uploads an Excel file to `POST /api/file/work-orders/upload`
- **THEN** the system parses work order numbers from column 1 using ClosedXML, looks up matching work orders in the database, deduplicates results, and returns a `FileUploadResult` with matching `WorkOrderViewModel` objects
#### Scenario: Generate and download work order template
- **WHEN** a user posts work order numbers to `POST /api/file/work-orders/template`
- **THEN** the system generates an Excel template using ClosedXML, caches it with a GUID key, and returns the key
- **WHEN** the user requests `GET /api/file/work-orders/template/{guid}`
- **THEN** the system retrieves the cached file and returns it as "work_order_template.xlsx"
#### Scenario: Upload component lots with item numbers
- **WHEN** a user uploads an Excel file with lot numbers and item numbers to `POST /api/file/component-lots/upload`
- **THEN** the system parses lot numbers from column 1 and item numbers from column 2, looks up matching lots, deduplicates, and returns matching `LotViewModel` objects
#### Scenario: Cached file expired
- **WHEN** a user requests a download with an expired or invalid cache key
- **THEN** the system returns HTTP 404 Not Found
#### Scenario: No file uploaded
- **WHEN** a user posts to an upload endpoint without a file
- **THEN** the system returns `FileUploadResult { WasSuccessful = false, ErrorMessage = "No file uploaded" }`
### Requirement: SignalR Real-Time Updates
The system SHALL provide real-time status updates to connected clients via ASP.NET Core SignalR hub.
#### Inputs
- Status updates from worker service (via `IHubContext<StatusHub>`)
- Search status changes (via `IHubContext<StatusHub>`)
- Client requests for cached status (GetCachedStatus hub method)
#### Outputs
- Broadcasts to all connected clients via `SendAsync("statusUpdate", ...)` and `SendAsync("searchUpdate", ...)`
#### Business Rules
- The system SHALL use ASP.NET Core SignalR (`Microsoft.AspNetCore.SignalR`)
- The system SHALL maintain a cached `StatusUpdate` with message and timestamp
- The system SHALL broadcast status updates to ALL connected clients using `Clients.All.SendAsync()`
- The system SHALL inject `IHubContext<StatusHub>` into controllers and services that need to publish updates
- The system SHALL NOT use static `GlobalHost` pattern - use DI exclusively
- The system SHALL initialize cached status with "Unknown" message and current timestamp
- The system SHALL map the hub endpoint to `/hubs/status`
#### Scenario: Worker service publishes status update
- **WHEN** the worker service publishes a `StatusUpdate` via `IHubContext<StatusHub>`
- **THEN** the system caches the update and broadcasts it to all connected clients via `Clients.All.SendAsync("statusUpdate", statusUpdate)`
#### Scenario: New search submitted via controller
- **WHEN** the SearchController saves a new search
- **THEN** the controller uses injected `IHubContext<StatusHub>` to call `Clients.All.SendAsync("searchUpdate", searchUpdate)`
#### Scenario: Client requests current status
- **WHEN** a newly connected client invokes the `GetCachedStatus` hub method
- **THEN** the system returns the most recent cached `StatusUpdate` (or the default "Unknown" status if none set)
#### Scenario: Multiple clients receive broadcast
- **WHEN** a status update is broadcast
- **THEN** all connected SignalR clients receive the update simultaneously via their `statusUpdate` or `searchUpdate` event handlers
### Requirement: Blazor Client Integration
The system SHALL support Blazor WebAssembly client authentication and real-time updates.
#### Authentication Flow
1. Blazor client calls `POST /api/auth/login` with credentials
2. Server validates credentials, creates cookie-based session
3. Server returns `UserInfo` JSON
4. Client stores user info in memory and sets authenticated state
5. Subsequent API calls include auth cookie automatically
#### SignalR Connection
1. Blazor client creates `HubConnection` to `/hubs/status`
2. Client registers handlers for `statusUpdate` and `searchUpdate` events
3. Client calls `GetCachedStatus()` on connection to get initial state
4. Client receives real-time updates via registered handlers
#### Business Rules
- The system SHALL use cookie authentication (not JWT) for browser-based Blazor WASM
- The system SHALL configure CORS for same-origin only by default; cross-origin support is NOT required for single-domain deployment
- The system SHALL return JSON responses (not redirects) for all API errors
- The system SHALL support reconnection for SignalR clients with automatic retry
- The SignalR hub SHALL remain open without requiring per-request authentication (cookies validated on connection)
- The Blazor client SHALL handle SPA navigation behaviors (return URL handling, 401 interception for redirect to login)
### Requirement: Authorization Patterns
The system SHALL enforce authorization using ASP.NET Core attribute-based access control.
#### Inputs
- Authorization attributes on controllers and actions
- User authentication status from cookie
#### Outputs
- Access granted or denied
- HTTP 401 Unauthorized for unauthenticated API requests
- HTTP 403 Forbidden for authenticated but unauthorized requests
#### Business Rules
- The system SHALL apply `[Authorize]` at controller level for `SearchController`
- The system SHALL apply `[AllowAnonymous]` for `AuthController.Login` action
- The system SHALL NOT require authorization for `LookupController` or `FileController` endpoints
- The system SHALL return HTTP 401 (not redirect) for unauthorized API requests to support Blazor WASM
- The system SHALL configure cookie authentication to suppress redirect on 401
#### Scenario: Authorized user accesses protected controller
- **WHEN** an authenticated user accesses a controller with `[Authorize]` attribute
- **THEN** the system allows the request to proceed to the action method
#### Scenario: Anonymous user accesses protected API
- **WHEN** an anonymous user accesses a controller with `[Authorize]` attribute
- **THEN** the system returns HTTP 401 Unauthorized (no redirect)
#### Scenario: User accesses public lookup endpoint
- **WHEN** any user (authenticated or not) accesses a `LookupController` endpoint
- **THEN** the system allows the request without authentication check
## Data Models
### UserInfo (formerly LDAPEntry)
```csharp
public class UserInfo
{
public string DN { get; set; } = string.Empty; // Distinguished name
public string Username { get; set; } = string.Empty; // sAMAccountName (lowercase)
public string FirstName { get; set; } = string.Empty; // givenName
public string LastName { get; set; } = string.Empty; // sn
public string DisplayName => string.IsNullOrWhiteSpace(FirstName) && string.IsNullOrWhiteSpace(LastName)
? Username
: $"{FirstName} {LastName}".Trim(); // Computed
public string Title { get; set; } = string.Empty; // title
public string EmailAddress { get; set; } = string.Empty; // mail
}
```
### StatusUpdate
```csharp
public class StatusUpdate
{
public string Message { get; set; } = string.Empty; // Update message to display
public DateTime Timestamp { get; set; } // When message was sent
}
```
### SearchUpdate
```csharp
public class SearchUpdate
{
public int ID { get; set; } // Search primary key
public string UserName { get; set; } = string.Empty; // Username who submitted
public string Name { get; set; } = string.Empty; // Search name
[JsonConverter(typeof(JsonStringEnumConverter))]
public SearchStatus Status { get; set; } // Enum: New, Submitted, Started, Ended, Error
public DateTime? SubmitDT { get; set; } // When submitted (required for UI grid)
public DateTime? StartDT { get; set; } // When processing started (required for UI grid)
public DateTime? EndDT { get; set; } // When processing ended (required for UI grid)
public DateTime Timestamp { get; set; } // When update was generated (required for ordering)
}
```
### FileUploadResult<T>
```csharp
public class FileUploadResult<T>
{
public bool WasSuccessful { get; set; }
public string? ErrorMessage { get; set; }
public T[]? Data { get; set; }
}
```
### LoginRequest
```csharp
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
```
### AuthResult
```csharp
public record AuthResult(bool Success, UserInfo? User, string? ErrorMessage);
```
## Migration Notes
| Legacy Pattern | New Pattern | Rationale |
|----------------|-------------|-----------|
| ASP.NET MVC 5 Controllers | ASP.NET Core Controllers with `[ApiController]` | Modern framework, automatic model binding and validation |
| `JsonNetResult` custom result | `Results.Ok(data)` or `return Ok(data)` | Built-in JSON support with System.Text.Json |
| Newtonsoft.Json `[JsonConverter(typeof(StringEnumConverter))]` | System.Text.Json `[JsonConverter(typeof(JsonStringEnumConverter))]` | Built-in serialization |
| Legacy SignalR (`Microsoft.AspNet.SignalR`) | ASP.NET Core SignalR (`Microsoft.AspNetCore.SignalR`) | Built-in, cross-platform |
| `GlobalHost.ConnectionManager.GetHubContext<T>()` | `IHubContext<T>` via dependency injection | Standard DI pattern, testable |
| `Clients.All.statusUpdate(...)` | `Clients.All.SendAsync("statusUpdate", ...)` | Async-first API |
| OWIN Authentication middleware | ASP.NET Core Authentication with `CookieAuthenticationDefaults` | Modern authentication stack |
| `HttpContext.GetOwinContext().Authentication.SignIn()` | `HttpContext.SignInAsync()` | No OWIN abstraction layer |
| `DefaultAuthenticationTypes.ApplicationCookie` | `CookieAuthenticationDefaults.AuthenticationScheme` | Standard scheme name |
| `System.DirectoryServices.DirectoryEntry` | `System.DirectoryServices.Protocols.LdapConnection` | Cross-platform LDAP support |
| `WebConfigurationManager.AppSettings` | `IOptions<LdapOptions>` / `IOptions<AuthOptions>` | Strongly-typed configuration |
| `MemoryCache.Default` | `IMemoryCache` via dependency injection | Standard caching abstraction |
| `Request.Files` collection | `IFormFile` parameter | Modern file upload handling |
| EPPlus library | ClosedXML library | MIT license (EPPlus changed to non-commercial) |
| Route-based MVC URLs (`/Search/GetSearches`) | Attribute routing (`/api/search`) | REST conventions, clear API structure |
| Login redirect for unauthorized | HTTP 401 response | Blazor WASM SPA compatibility |
| NLog for logging | `ILogger<T>` injected + `BeginScope()` for context | Built-in logging abstraction |
| `LDAPEntry` model | `UserInfo` model | Clearer naming, not tied to LDAP implementation |
| Hardcoded user exception in code | Configurable via `AuthOptions.AdminBypassUsers` | Production-safe configuration; empty array by default |
## Codex Review Findings (Status)
The following issues were identified during review and their resolution status:
| Finding | Status | Resolution |
|---------|--------|------------|
| Missing Account Endpoints | Resolved | New `/api/auth/*` endpoints documented |
| Controller Name Typo (`SessionController` vs `SessionsController`) | Resolved | Legacy session endpoints not needed - Blazor WASM handles client-side routing |
| InvalidUA Route | Resolved | Not needed - Blazor WASM handles user agent detection client-side |
| LDAP DisplayName Fallback | Clarified | Spec now correctly documents computed `DisplayName` logic |
| File Upload Error Handling | Documented | Per-row parse errors swallowed, specific error messages documented |
| SignalR Best-Effort | Documented | SignalR publish is best-effort, exceptions logged but swallowed |
## Open Questions (Resolved)
| Question | Decision | Rationale |
|----------|----------|-----------|
| LDAP Failover Strategy | Keep simple sequential | Simple sequential failover is sufficient for this use case. Health checks and circuit breaker add complexity without significant benefit given the expected load. |
| Authorization Granularity | Group membership only | The legacy system only requires group membership. More granular RBAC is not needed for this tool. |
| SignalR Connection Management | Broadcast to all | Broadcasting to all clients is acceptable since the status updates are not user-specific and the number of concurrent users is expected to be small. |
| API Authentication | Cookie-based only | JWT tokens add complexity without benefit for this internal browser-based tool. Cookie auth is simpler and more secure for browser clients. |
| File Upload Security | Keep anonymous | Maintain legacy behavior. File uploads only parse Excel data; they don't access protected resources. |
| Rate Limiting | Not implemented initially | Can be added later if needed. Current user base is small and internal. |
| Hardcoded User Exception | Configurable admin bypass | Move to `AuthOptions.AdminBypassUsers` configuration array (empty by default). Allows dev flexibility without hardcoding. |