QDG Knowledge Base Read-only viewer QWebHub
general

API Reference

Version 1 · Initial page: full endpoint reference for QDG.Migration.Web, grouped by controller, plus auth/antiforgery conventions and known inconsistencies.

API Reference

Every reachable route in QDG.Migration.Web, grouped by controller. "Page routes" render a View or redirect for browser navigation; "JSON endpoints" are meant to be called via fetch/AJAX from a page's own JavaScript and return Ok(...)/Json(...)/BadRequest(...).

Conventions that apply to every action

  • Auth is required by default. Program.cs registers a global AuthorizeFilter (RequireAuthenticatedUser()) on top of MVC — every controller/action needs an authenticated user unless it (or its controller) carries [AllowAnonymous]. Only AccountController opts out.
  • JWT comes from a cookie, not a header. The bearer scheme reads Request.Cookies["AuthToken"] (Program.cs, OnMessageReceived), not Authorization: Bearer. An expired/invalid token triggers a redirect to /Account/Login (OnChallenge), not a bare 401 — relevant even for JSON endpoints, since a client polling a JSON route with an expired cookie will get an HTML redirect response, not a clean error status.
  • SilentTokenRefreshFilter runs on every authenticated request (except Account actions). If the embedded external access token is within 120s of its 900s expiry, it silently refreshes via ICentralAuthService.RefreshAsync and reissues the AuthToken cookie before the request continues; if refresh fails, it clears the cookie and redirects to /Account/Login?reason=expired.
  • Roles: Admin, Operator, Viewer (QDG.Migration.Core/Models/UserRoles.cs), plus the convenience constant AdminOrOperator = "Admin,Operator" used throughout.
  • Antiforgery: form-posting page actions consistently carry [ValidateAntiForgeryToken]. Several JSON endpoints intentionally omit it (bearer-cookie-only AJAX calls) — see "Known inconsistencies" below for where this coverage isn't uniform.
  • Routing: conventional {controller=Home}/{action=Index}/{id?}, plus explicit [Http*] attribute routes (often api/...) on individual actions within the same controllers — attribute and conventional routing are mixed, not separated into dedicated API controllers except for the three under Controllers/Api/.

AccountController — /Account (class-level [AllowAnonymous])

Action Route Params Returns
Login (GET) GET /Account/Login string? reason View (login form)
Login (POST) POST /Account/Login email, password (form) Redirect to Verify, or View with error
Verify (GET) GET /Account/Verify pendingToken, email View (2FA code form)
Verify (POST) POST /Account/Verify pendingToken, email, code Sets AuthToken cookie, redirect to Home/Index, or View with error
Logout (POST) POST /Account/Logout none Clears cookie, redirect to Login

Two-step login (email+password → 6-digit TOTP) against ICentralAuthService; on success, IAuthTokenService.CreateSessionToken mints this app's own session JWT into the AuthToken cookie.

HomeController — /Home (also site root /)

Action Route Returns
Index GET / or /Home/Index View, sets ViewBag.HasActiveConfig
Error GET /Home/Error View (also wired as the non-Dev UseExceptionHandler target)

ConfigurationController — /Configuration

Action Route Role Params Returns
Index GET /Configuration any authenticated none View (config list)
Create (GET) GET /Configuration/Create AdminOrOperator none View
Create (POST) POST /Configuration/Create AdminOrOperator ConfigurationViewModel View or redirect Index; encrypts passwords via EncryptionHelper.Encrypt
Edit (GET) GET /Configuration/Edit/{id} AdminOrOperator int id View or 404
Edit (POST) POST /Configuration/Edit AdminOrOperator ConfigurationViewModel View or redirect Index
Delete POST /Configuration/Delete Admin only int id Redirect Index
SetActive POST /Configuration/SetActive AdminOrOperator int id Redirect Index

BarrierTrialsController — /BarrierTrials

Action Route Role Params Returns
Index GET /BarrierTrials any authenticated none View
Preview POST /BarrierTrials/Preview AdminOrOperator IFormFile excelFile View (preview) or redirect with error
AutoSync GET /BarrierTrials/AutoSync AdminOrOperator none View (settings + preview)
SaveAutoSync POST /BarrierTrials/SaveAutoSync AdminOrOperator enabled/interval/FK/filter config, optional Excel file Redirect to AutoSync
ToggleAutoSync POST /api/barriertrials/autosync/toggle AdminOrOperator none JSON {enabled, nextRunAt}
RunAutoSyncNow POST /api/barriertrials/autosync/run-now AdminOrOperator none JSON {message}, fires background Task.Run
Start POST /BarrierTrials/Start AdminOrOperator tempPath, FK/filter config Redirect to Progress, starts background copy via Task.Run
ClearData POST /BarrierTrials/ClearData Admin only none Redirect Index, truncates destination tables
LiveCounts GET /api/barriertrials/livecounts any authenticated none JSON {sourceRowCount, destinationCounts, fkMappingCounts}
TestFilter GET /api/barriertrials/testfilter any authenticated filter (query) JSON {success, count} or {success:false, error}
Progress GET /BarrierTrials/Progress any authenticated Guid jobId View
Cancel POST /BarrierTrials/Cancel any authenticated Guid jobId Redirect to Progress
GetProgress GET /api/barriertrials/progress/{jobId} any authenticated Guid jobId (route) JSON progress/log/summary, polled by the Progress page

SectionalController — /Sectional

Mirrors BarrierTrialsController exactly, same auth tiers, one-for-one:

Index (GET, any authenticated) · Preview (POST, AdminOrOperator) · AutoSync (GET, AdminOrOperator) · SaveAutoSync (POST, AdminOrOperator) · POST /api/sectional/autosync/toggle (AdminOrOperator, JSON) · POST /api/sectional/autosync/run-now (AdminOrOperator, JSON) · Start (POST, AdminOrOperator) · ClearData (POST, Admin) · GET /api/sectional/livecounts (any authenticated, JSON) · GET /api/sectional/testfilter?filter= (any authenticated, JSON) · Progress (GET, any authenticated) · Cancel (POST, any authenticated) · GET /api/sectional/progress/{jobId} (any authenticated, JSON).

DisciplineRepairController — /DisciplineRepair

Action Route Role Params Returns
Index GET /DisciplineRepair any authenticated none View
Start POST /DisciplineRepair/Start AdminOrOperator none Redirect to Progress, background Task.Run, in-process job registry
Progress GET /DisciplineRepair/Progress any authenticated Guid jobId View
Cancel POST /DisciplineRepair/Cancel any authenticated Guid jobId Redirect to Progress
GetProgress GET /api/discipline-repair/progress/{jobId} any authenticated Guid jobId (route) JSON progress/log/summary

MeetingsController — /Meetings

Action Route Params Returns
Index GET /Meetings string? rsId, string? qdgId View — raw SQL source-vs-destination comparison
Races GET /Meetings/Races int rsMeetingId JSON list of comparison rows, AJAX drill-down
Runners GET /Meetings/Runners int rsRaceId, int? qdgRaceId JSON list of comparison rows, AJAX drill-down

MigrationController — /Migration

The wizard flow controller (largest in the app). Only RunConfig (POST), ClearHistoryAjax, and ClearMigrationHistory are gated to AdminOrOperator; everything else below only requires "any authenticated user" — see "Known inconsistencies."

Action Route Role Params Returns
TableSelection (GET) GET /Migration/TableSelection AdminOrOperator none View, creates a new MigrationSession
TableSelection (POST) POST /Migration/TableSelection AdminOrOperator Guid sessionId, string selectedTables Redirect to SchemaImport
SchemaImport (GET) GET /Migration/SchemaImport any authenticated Guid sessionId View or 404
SchemaImportUseSaved POST /Migration/SchemaImportUseSaved any authenticated Guid sessionId Redirect to ReviewMappings, reuses saved schema file
SchemaImport (POST) POST /Migration/SchemaImport any authenticated Guid sessionId, IFormFile? schemaFile Redirect to ReviewMappings
ReviewMappings (GET/POST) /Migration/ReviewMappings any authenticated Guid sessionId[, List<FieldMappingEdit> Mappings] View / redirect to RelationshipConfig
ValidateSchema GET /Migration/ValidateSchema any authenticated Guid sessionId View (validation result)
RunConfig (GET) GET /Migration/RunConfig any authenticated Guid sessionId View, computes FK-based order
RunConfig (POST) POST /Migration/RunConfig AdminOrOperator Guid sessionId, List<TableFilterItem> filters, bool patchMode=false Redirect to Progress — starts the migration
ClearHistoryAjax POST /Migration/ClearHistoryAjax AdminOrOperator Guid sessionId, [FromBody] List<string>? tableNames JSON {deleted}
FilterConfig (GET/POST) /Migration/FilterConfig any authenticated Guid sessionId[, filters] View / redirect to RelationshipConfig
RelationshipConfig (GET/POST) /Migration/RelationshipConfig any authenticated Guid sessionId[, columnConfigs, tableOrder] View / redirect to RunConfig
AddCustomRelationship POST /Migration/AddCustomRelationship any authenticated Guid sessionId, parentTable, parentColumn, childTable, childColumn JSON {success, id, message}
GetDestinationTableColumns GET /Migration/GetDestinationTableColumns any authenticated string tableName JSON [{ColumnName, DataType, IsPrimaryKey}]
DeleteRelationship POST /Migration/DeleteRelationship any authenticated int relationshipId JSON {success, message}
FkPatch (GET/POST) /Migration/FkPatch any authenticated childSourceTable, fkSourceColumn, parentSourceTable View — standalone FK-backfill tool, no session required
FieldMapping (GET) GET /Migration/FieldMapping any authenticated Guid sessionId View
FieldMappingPost POST /Migration/FieldMappingPost any authenticated Guid sessionId, mappings, naturalKeys Redirect to Summary
FindNaturalKeyMatches POST /Migration/FindNaturalKeyMatches any authenticated Guid sessionId Redirect to PendingMappings
PendingMappings (GET) GET /Migration/PendingMappings any authenticated Guid sessionId View
ConfirmMapping / RejectMapping / AddManualMapping POST /Migration/... any authenticated mapping id / manual source+dest ids Redirect to PendingMappings
Summary GET /Migration/Summary any authenticated Guid sessionId View
StartMigration POST /Migration/StartMigration any authenticated Guid sessionId Redirect to Progress, background Task.Run
CancelMigration POST /Migration/CancelMigration any authenticated Guid sessionId JSON {message}
Progress GET /Migration/Progress any authenticated Guid sessionId View
Results GET /Migration/Results any authenticated Guid sessionId View
BackfillForeignKeys / BackfillBooleanColumns POST /Migration/Backfill... any authenticated Guid sessionId Redirect to Results
ClearMigrationHistory POST /Migration/ClearMigrationHistory AdminOrOperator Guid sessionId, tableNames? Redirect to Results
SyncIdMappingsToDestination POST /Migration/SyncIdMappingsToDestination AdminOrOperator Guid sessionId Redirect to Results, writes SQLite mappings into MySQL config_id_mappings

SessionController — /Session

Action Route Params Returns
Index GET /Session MigrationStatus? status, sortBy="created", descending=true View (session list + stats)
Delete POST /Session/Delete Guid sessionId Redirect to Index

SyncController — /Sync (class-level [Authorize], no role restriction on any action)

Page/CRUD actions:

Action Route Params Returns
Index GET /Sync none View (jobs list)
Create GET /Sync/Create none View
Edit GET /Sync/Edit/{id} int id View or 404
Save POST /Sync/Save SyncJobViewModel Redirect Index or re-render Edit
Delete POST /Sync/Delete/{id} int id Redirect Index
UploadSchema POST /Sync/UploadSchema?configId= int configId, IFormFile? schemaFile JSON {message} / {error}
Preview GET /Sync/Preview/{id} int id View (mapping/FK preview)

/api/sync/... JSON endpoints (several POSTs here omit [ValidateAntiForgeryToken] — see "Known inconsistencies"):

Action Route Params Returns
Toggle POST /api/sync/{id}/toggle int id JSON {enabled, nextRunAt}
RunNow POST /api/sync/{id}/run-now int id JSON {message}, fire-and-forget on a fresh DI scope
SessionStatus GET /api/sync/{id}/session-status int id JSON {currentSessionGuid, status}, polled by the "Run Now" button
Status GET /api/sync/status none JSON {running, errored, enabled, total}, navbar indicator
GetTables GET /api/sync/tables?configId= int configId JSON array of table names
GetColumns GET /api/sync/columns?configId=&table= int configId, string table JSON array {ColumnName, DataType}
GetDestinationTables GET /api/sync/destination-tables?configId= int configId JSON array of destination table names
GetDestinationColumns GET /api/sync/destination-columns?configId=&table= int configId, string table JSON array {ColumnName, DataType}
SchemaStatus GET /api/sync/schema-status?configId= int configId JSON {exists}
GetTableMappings GET /api/sync/table-mappings?configId=&table= int configId, string table JSON {found, destTable, columns[]}
GetTableFks GET /api/sync/table-fks?configId=&table= int configId, string table JSON array of merged detected+custom FKs

Controllers/Api/ — dedicated [ApiController]s

ConfigurationApiController — api/configuration

Action Route Params Returns
TestConnection POST api/configuration/test-connection {Host, Port, Database, Username, Password} (body) JSON {success, message} (always 200, success flag in body)

MigrationApiController — api/migration

Action Route Params Returns
GetProgress GET api/migration/progress/{sessionId} Guid sessionId (route) JSON progress payload; 404 if session missing, 500 on exception. Polled by the wizard's Progress view
GetLogs GET api/migration/logs/{sessionId} Guid sessionId (route), int count=50 JSON array {level, message, tableName, timestamp}

TableApiController — api/table

Action Route Params Returns
TestFilter POST api/table/test-filter {TableName, Filter, OriginalCount} (body) JSON {success, count, originalCount} or {success:false, error}
GetTableInfo GET api/table/info/{tableName} string tableName (route) JSON TableInfo; 400 if no active config, 500 on error

Shared pattern: background work + progress polling

Four features repeat the same shape rather than each having a bespoke design:

  1. A POST action (RunConfig/StartMigration, BarrierTrials/Start, Sectional/Start, DisciplineRepair/Start) launches the work on Task.Run and immediately redirects to a Progress view.
  2. The Progress view polls a paired GET .../progress/{id} JSON endpoint on an interval.
  3. Background work always resolves its services from a fresh IServiceScopeFactory.CreateScope(), never the controller's own request-scoped services — the request's DI scope is disposed the moment the action returns, before the background task finishes.

Known inconsistencies

  • Role gating is uneven. Most of MigrationController's wizard actions (including StartMigration, CancelMigration, AddCustomRelationship, DeleteRelationship, FkPatch POST) only require "any authenticated user," so a Viewer can trigger them — likely not intentional, worth reviewing rather than assuming it's by design. SyncController and SessionController.Delete are similarly unrestricted by role.
  • Antiforgery coverage on JSON POSTs is inconsistent. SyncController.Toggle/RunNow and BarrierTrialsController/SectionalController's ToggleAutoSync/RunAutoSyncNow all omit [ValidateAntiForgeryToken], while page-form POSTs consistently include it. Plausibly intentional (cookie-bearer AJAX calls without antiforgery header wiring), but not uniform, so don't assume any single JSON POST is protected without checking it directly.
  • SessionController.Delete has no [ValidateAntiForgeryToken] at all, unlike equivalent delete actions elsewhere (ConfigurationController.Delete, SyncController.Delete).

Dead code: Blazor pages are unreachable

Components/Pages/Home.razor, Configuration.razor, and Sessions.razor declare @page routes (/, /configuration, /sessions) with @rendermode InteractiveServer, but Program.cs never calls AddRazorComponents()/AddInteractiveServerComponents() or maps MapRazorComponents<App>() — only MapControllerRoute and MapRazorPages() are registered. Requests to those paths resolve to the MVC HomeController/ConfigurationController/ SessionController instead. Do not document these Razor components as live endpoints; they appear to be leftovers from an earlier Blazor-based prototype superseded by the MVC controllers and views.

Updated by Claude on Aug. 12, 2026, 8:27 a.m. · Task: Create API documentation for QDG.Migration.Web