Switched Azure Function to ASP.net
Some checks failed
CI Build and Deploy / build (push) Has been cancelled

This commit is contained in:
Grae Jones
2026-03-21 08:21:17 -07:00
parent 967f04ebbc
commit 9fa2c774a7
3 changed files with 177 additions and 71 deletions

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker; // ← SWAP: remove for AspNetCore
using Microsoft.Extensions.Logging;
using Registration.Data;
using System.Security.Claims;
@@ -17,13 +17,33 @@ namespace Registration.Functions;
/// entraSubjectId is extracted from the validated
/// token — the client never supplies it.
///
/// GET /api/registration/pending — Admin; requires Function key.
/// GET /api/registration/item/{id} — Admin; requires Function key.
/// POST /api/registration/action/{id}/reject — Admin; requires Function key.
/// POST /api/registration/action/{id}/complete — Admin; requires Function key.
/// GET /api/registration/pending — Admin; requires internal API key.
/// GET /api/registration/item/{id} — Admin; requires internal API key.
/// POST /api/registration/action/{id}/reject — Admin; requires internal API key.
/// POST /api/registration/action/{id}/complete — Admin; requires internal API key.
/// GET /api/health — Anonymous.
///
/// ═══════════════════════════════════════════════════════
/// HOST SWAP — three files change, everything else stays:
/// 1. Functions/RegistrationFunctions.cs ← this file
/// 2. Program.cs
/// 3. Registration.csproj
///
/// To switch to ASP.NET Core:
/// a) In this file: swap class declaration and method signatures (marked below)
/// b) In Program.cs: comment Azure Functions block, uncomment AspNetCore block
/// c) In Registration.csproj: swap ItemGroup (marked in that file)
/// d) In authConfig.js (client): update API_BASE_URL, remove API_FUNCTION_KEY
/// ═══════════════════════════════════════════════════════
/// </summary>
// ── SWAP: Azure Functions class declaration ───────────────────────────────
public class RegistrationFunctions
// ── SWAP: AspNetCore class declaration ────────────────────────────────────
// [ApiController]
// [Route("api")]
// public class RegistrationFunctions : ControllerBase
// ─────────────────────────────────────────────────────────────────────────
{
private readonly IRegistrationDataService _data;
private readonly ILogger<RegistrationFunctions> _log;
@@ -54,21 +74,21 @@ public class RegistrationFunctions
// ── Public: Register ─────────────────────────────────────────────────
/// <summary>
/// Register a new prospect.
///
/// AuthorizationLevel.Anonymous at the trigger allows any caller with a valid
/// Bearer token — no Function key required from the browser.
/// The [Authorize] attribute ensures the JWT is present and valid before
/// the function body runs. entraSubjectId is extracted from token claims only.
/// </summary>
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Register")]
[Authorize]
public async Task<IActionResult> Register(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/register")] HttpRequest req,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpPost("registration/register")]
// [Authorize]
// public async Task<IActionResult> Register(CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// Identity comes from the validated JWT — never from the request body
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
var entraSubjectId = GetEntraSubjectId(req.HttpContext.User);
if (string.IsNullOrEmpty(entraSubjectId))
@@ -90,7 +110,6 @@ public class RegistrationFunctions
if (request == null || string.IsNullOrWhiteSpace(request.BusinessName))
return new BadRequestObjectResult(new { ok = false, error = "businessName is required" });
// Stamp the server-validated identity — client-supplied value is ignored
request.EntraSubjectId = entraSubjectId;
_log.LogInformation("[Registration] POST register: {Name} by entra={EntraId}",
@@ -106,11 +125,15 @@ public class RegistrationFunctions
// ── Admin: List pending ───────────────────────────────────────────────
/// <summary>List all pending registrations. Called by Management API with Function key.</summary>
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("GetPending")]
public async Task<IActionResult> GetPending(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/pending")] HttpRequest req,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpGet("registration/pending")]
// public async Task<IActionResult> GetPending(CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
_log.LogInformation("[Registration] GET pending");
var result = await _data.GetPendingAsync(ct);
@@ -119,12 +142,16 @@ public class RegistrationFunctions
// ── Admin: Get by ID ─────────────────────────────────────────────────
/// <summary>Get a single applicant by registration ID. Called by Management API.</summary>
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("GetById")]
public async Task<IActionResult> GetById(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/item/{registrationId}")] HttpRequest req,
string registrationId,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpGet("registration/item/{registrationId}")]
// public async Task<IActionResult> GetById(string registrationId, CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
_log.LogInformation("[Registration] GET {Id}", registrationId);
@@ -137,13 +164,20 @@ public class RegistrationFunctions
// ── Admin: Reject ────────────────────────────────────────────────────
/// <summary>Reject a pending applicant. Called by Management after admin clicks Reject.</summary>
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Reject")]
public async Task<IActionResult> Reject(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/reject")] HttpRequest req,
string registrationId,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpPost("registration/action/{registrationId}/reject")]
// public async Task<IActionResult> Reject(string registrationId, CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
RejectBody? body = null;
try { body = await JsonSerializer.DeserializeAsync<RejectBody>(req.Body, JsonOpts, ct); }
catch { /* optional body */ }
@@ -159,19 +193,22 @@ public class RegistrationFunctions
return new OkObjectResult(result);
}
// ── Admin: Complete (approved) ────────────────────────────────────────
// ── Admin: Complete ───────────────────────────────────────────────────
/// <summary>
/// Mark a registration as approved/completed.
/// Called by Management after spClientManagement.create succeeds.
/// Receives the platformClientId to link the registration to the platform record.
/// </summary>
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Complete")]
public async Task<IActionResult> Complete(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/complete")] HttpRequest req,
string registrationId,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpPost("registration/action/{registrationId}/complete")]
// public async Task<IActionResult> Complete(string registrationId, CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
CompleteBody? body = null;
try { body = await JsonSerializer.DeserializeAsync<CompleteBody>(req.Body, JsonOpts, ct); }
catch { /* optional body */ }
@@ -189,10 +226,14 @@ public class RegistrationFunctions
// ── Health ───────────────────────────────────────────────────────────
/// <summary>Health check — anonymous, no auth required.</summary>
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Health")]
public IActionResult Health(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequest req)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpGet("health")]
// public IActionResult Health()
// ─────────────────────────────────────────────────────────────────────
{
return new OkObjectResult(new
{
@@ -215,4 +256,4 @@ internal sealed class RejectBody
internal sealed class CompleteBody
{
public string? PlatformClientId { get; set; }
}
}