// Hands-On tutorial — MVC controller that backs the asset // related-list's Header Action / Item Action. Hosted by the BFF // because the React `Refresh / Probe` buttons hit `/api/v2/handson/ // assets/*` over the shell origin and nginx routes `/api/` to the // BFF. // // These endpoints are intentionally stubs — real implementations // would push to a queue, kick off an ffprobe job, write back to // SIZE_BYTES, etc. The tutorial verifies the React → BFF wiring // (event bus, refresh propagation, toast UX) without depending on // any actual media processing. using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Acme.Mib.HandsOn.Controllers; [ApiController] // `[AllowAnonymous]` — intentional for the tutorial: // // The BFF's `app.UseAuthentication()` runs the default `API` policy // scheme on every request. That scheme forwards to JwtBearer when the // request carries `Authorization: Bearer ` (the React shell's // `window.cms.instance` always does), and the BFF's JwtBearer setup // installs a `MibSecurityTokenValidator` that round-trips the token // to the AuthorizationServer's userinfo endpoint plus a permissions // check. For OUR plain (non-RazorBaseController) controller, that // round-trip ends in a `NotAuthorizedException` that the WorkflowFactory- // Middleware catches and turns into a 401. The shell's axios response // interceptor reads the 401 and bounces the user to /login — which // looks like the action "navigates away" instead of running. // // `[AllowAnonymous]` opts our controller out of both the [Authorize] // gate and the WorkflowFactoryMiddleware UseWhen branch (which only // fires when an [Authorize] attribute is present on the endpoint). The // trade-off: the Bearer-token identity is NOT available inside the // action body. That's fine for stub demo endpoints; a real-world // /api/v2 controller that needs `User.Identity.Name` should either // inherit from `ApiBaseController` (which carries the correct // `[Authorize(AuthenticationSchemes = Constants.API_SCHEME)]` attr and // has full BFF DI plumbing) or replicate that attribute and provide its // own equivalent WorkflowFactory setup. [AllowAnonymous] [Route("api/v2/handson/assets")] public sealed class AssetsController : ControllerBase { private readonly ILogger _log; public AssetsController(ILogger log) => _log = log; // Header Action — re-scan ALL assets. [HttpPost("reindex")] public IActionResult ReindexAll() { _log.LogInformation("Re-scanning all HANDSON_ASSETS"); // Real implementation: enqueue a job. Demo: pretend a few // were scanned. var affected = 7; return Ok(new { affected, status = "queued" }); } // Item Action — probe a single asset's media metadata. Returns // a fake duration + size payload so the React toast has // something to show. A real implementation would read from // ffprobe / Storage and persist the values back to the row. [HttpPost("{id:int}/probe")] public IActionResult ProbeOne(int id) { _log.LogInformation("Probing HANDSON_ASSETS.id={Id}", id); // Deterministic stub so the demo is reproducible per id. var duration = 60 + (id % 240); // seconds var size = 256_000 + (id * 17_000); // bytes return Ok(new { id, duration, size }); } }