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.csregisters a globalAuthorizeFilter(RequireAuthenticatedUser()) on top of MVC — every controller/action needs an authenticated user unless it (or its controller) carries[AllowAnonymous]. OnlyAccountControlleropts out. - JWT comes from a cookie, not a header. The bearer scheme reads
Request.Cookies["AuthToken"](Program.cs,OnMessageReceived), notAuthorization: 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. SilentTokenRefreshFilterruns on every authenticated request (exceptAccountactions). If the embedded external access token is within 120s of its 900s expiry, it silently refreshes viaICentralAuthService.RefreshAsyncand reissues theAuthTokencookie 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 constantAdminOrOperator = "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 (oftenapi/...) on individual actions within the same controllers — attribute and conventional routing are mixed, not separated into dedicated API controllers except for the three underControllers/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:
- A POST action (
RunConfig/StartMigration,BarrierTrials/Start,Sectional/Start,DisciplineRepair/Start) launches the work onTask.Runand immediately redirects to aProgressview. - The
Progressview polls a pairedGET .../progress/{id}JSON endpoint on an interval. - 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 (includingStartMigration,CancelMigration,AddCustomRelationship,DeleteRelationship,FkPatchPOST) only require "any authenticated user," so aViewercan trigger them — likely not intentional, worth reviewing rather than assuming it's by design.SyncControllerandSessionController.Deleteare similarly unrestricted by role. - Antiforgery coverage on JSON POSTs is inconsistent.
SyncController.Toggle/RunNowandBarrierTrialsController/SectionalController'sToggleAutoSync/RunAutoSyncNowall 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.Deletehas 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.