Revised Registration
All checks were successful
Registration / build-deploy (push) Successful in 9m8s

This commit is contained in:
Grae Jones
2026-03-22 09:37:28 -07:00
parent 8de463cd17
commit fae2226581
6 changed files with 389 additions and 215 deletions

View File

@@ -1,12 +1,21 @@
// ── SWAP note ─────────────────────────────────────────────────────────────
// Three files change when switching host modes. See Registration.csproj for
// the full checklist. This file: swap the class declaration and each method
// signature (marked below). Program.cs and Registration.csproj also change.
// ─────────────────────────────────────────────────────────────────────────
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker; // ← SWAP: remove for AspNetCore
using Microsoft.Extensions.Logging;
using Registration.Auth;
using Registration.Data;
using System.Security.Claims;
using System.Text.Json;
// ── SWAP: Azure Functions — uncomment this using when restoring Functions mode
// using Microsoft.Azure.Functions.Worker;
namespace Registration.Functions;
/// <summary>
@@ -17,10 +26,10 @@ namespace Registration.Functions;
/// entraSubjectId is extracted from the validated
/// token — the client never supplies it.
///
/// 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/registration/pending — Admin; x-functions-key header required.
/// GET /api/registration/item/{id} — Admin; x-functions-key header required.
/// POST /api/registration/action/{id}/reject — Admin; x-functions-key header required.
/// POST /api/registration/action/{id}/complete — Admin; x-functions-key header required.
/// GET /api/health — Anonymous.
///
/// ═══════════════════════════════════════════════════════
@@ -29,20 +38,23 @@ namespace Registration.Functions;
/// 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
/// To switch back to Azure Functions:
/// a) Swap class declaration (marked below)
/// b) Swap each method signature (marked below)
/// c) Uncomment [Function(...)] and [HttpTrigger(...)] attributes
/// d) Re-add req parameter and remove [ApiKeyAuth] on admin endpoints
/// (Functions mode uses AuthorizationLevel.Function instead)
/// e) In authConfig.js: update API_BASE_URL to Function App URL,
/// set API_FUNCTION_KEY from Azure Portal → App Keys → default
/// ═══════════════════════════════════════════════════════
/// </summary>
// ── SWAP: Azure Functions class declaration ───────────────────────────────
public class RegistrationFunctions
// ── SWAP: AspNetCore class declaration ────────────────────────────────────
// [ApiController]
// [Route("api")]
// public class RegistrationFunctions : ControllerBase
// ── SWAP: ASP.NET Core class declaration ◄ ACTIVE ───────────────────────
[ApiController]
[Route("api")]
public class RegistrationFunctions : ControllerBase
// ── SWAP: Azure Functions class declaration ◄ INACTIVE — uncomment to restore
// public class RegistrationFunctions
// ─────────────────────────────────────────────────────────────────────────
{
private readonly IRegistrationDataService _data;
@@ -58,7 +70,7 @@ public class RegistrationFunctions
public RegistrationFunctions(IRegistrationDataService data, ILogger<RegistrationFunctions> log)
{
_data = data;
_log = log;
_log = log;
}
// ── Helpers ──────────────────────────────────────────────────────────
@@ -66,6 +78,7 @@ public class RegistrationFunctions
/// <summary>
/// Extract the Entra Object ID from a validated CIAM JWT.
/// The OID claim is stable across sessions and providers (Google, Apple, Microsoft).
/// CIAM tenant: PositiveSpendClients.ciamlogin.com / cbf8b7d7-1e13-486d-b5b0-287ba79fdf0b
/// </summary>
private static string? GetEntraSubjectId(ClaimsPrincipal user) =>
user.FindFirst("oid")?.Value
@@ -74,20 +87,21 @@ public class RegistrationFunctions
// ── Public: Register ─────────────────────────────────────────────────
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Register")]
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
[HttpPost("registration/register")]
[Authorize]
public async Task<IActionResult> Register(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/register")] HttpRequest req,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpPost("registration/register")]
public async Task<IActionResult> Register(CancellationToken ct)
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
// [Function("Register")]
// [Authorize]
// public async Task<IActionResult> Register(CancellationToken ct)
// public async Task<IActionResult> Register(
// [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/register")] HttpRequest req,
// CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
// ── SWAP: ASP.NET Core uses HttpContext.Request ───────────────────
var req = HttpContext.Request;
// ── SWAP: Azure Functions — req is passed as parameter, remove above line
var entraSubjectId = GetEntraSubjectId(req.HttpContext.User);
@@ -110,6 +124,7 @@ public class RegistrationFunctions
if (request == null || string.IsNullOrWhiteSpace(request.BusinessName))
return new BadRequestObjectResult(new { ok = false, error = "businessName is required" });
// Stamp from the validated token — never trust the request body for this.
request.EntraSubjectId = entraSubjectId;
_log.LogInformation("[Registration] POST register: {Name} by entra={EntraId}",
@@ -125,14 +140,15 @@ public class RegistrationFunctions
// ── Admin: List pending ───────────────────────────────────────────────
// ── 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)
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
[HttpGet("registration/pending")]
[ApiKeyAuth]
public async Task<IActionResult> GetPending(CancellationToken ct)
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
// [Function("GetPending")]
// public async Task<IActionResult> GetPending(
// [HttpTrigger(AuthorizationLevel.Function, "get", Route = "registration/pending")] HttpRequest req,
// CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
_log.LogInformation("[Registration] GET pending");
@@ -142,15 +158,16 @@ public class RegistrationFunctions
// ── Admin: Get by ID ─────────────────────────────────────────────────
// ── 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)
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
[HttpGet("registration/item/{registrationId}")]
[ApiKeyAuth]
public async Task<IActionResult> GetById(string registrationId, CancellationToken ct)
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
// [Function("GetById")]
// public async Task<IActionResult> GetById(
// [HttpTrigger(AuthorizationLevel.Function, "get", Route = "registration/item/{registrationId}")] HttpRequest req,
// string registrationId,
// CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
_log.LogInformation("[Registration] GET {Id}", registrationId);
@@ -164,19 +181,21 @@ public class RegistrationFunctions
// ── Admin: Reject ────────────────────────────────────────────────────
// ── 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)
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
[HttpPost("registration/action/{registrationId}/reject")]
[ApiKeyAuth]
public async Task<IActionResult> Reject(string registrationId, CancellationToken ct)
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
// [Function("Reject")]
// public async Task<IActionResult> Reject(
// [HttpTrigger(AuthorizationLevel.Function, "post", Route = "registration/action/{registrationId}/reject")] HttpRequest req,
// string registrationId,
// CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
// ── SWAP: ASP.NET Core ────────────────────────────────────────────
var req = HttpContext.Request;
// ── SWAP: Azure Functions — req is passed as parameter, remove above line
RejectBody? body = null;
try { body = await JsonSerializer.DeserializeAsync<RejectBody>(req.Body, JsonOpts, ct); }
@@ -195,19 +214,21 @@ public class RegistrationFunctions
// ── Admin: Complete ───────────────────────────────────────────────────
// ── 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)
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
[HttpPost("registration/action/{registrationId}/complete")]
[ApiKeyAuth]
public async Task<IActionResult> Complete(string registrationId, CancellationToken ct)
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
// [Function("Complete")]
// public async Task<IActionResult> Complete(
// [HttpTrigger(AuthorizationLevel.Function, "post", Route = "registration/action/{registrationId}/complete")] HttpRequest req,
// string registrationId,
// CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
// ── SWAP: ASP.NET Core ────────────────────────────────────────────
var req = HttpContext.Request;
// ── SWAP: Azure Functions — req is passed as parameter, remove above line
CompleteBody? body = null;
try { body = await JsonSerializer.DeserializeAsync<CompleteBody>(req.Body, JsonOpts, ct); }
@@ -226,20 +247,21 @@ public class RegistrationFunctions
// ── Health ───────────────────────────────────────────────────────────
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Health")]
public IActionResult Health(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequest req)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpGet("health")]
// public IActionResult Health()
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
[HttpGet("health")]
[AllowAnonymous]
public IActionResult Health()
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
// [Function("Health")]
// public IActionResult Health(
// [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequest req)
// ─────────────────────────────────────────────────────────────────────
{
return new OkObjectResult(new
{
ok = true,
service = "registration",
mode = _data is Registration.Mock.MockDataService ? "mock" : "database",
ok = true,
service = "registration",
mode = _data is Registration.Mock.MockDataService ? "mock" : "database",
timestamp = DateTime.UtcNow
});
}
@@ -249,11 +271,11 @@ public class RegistrationFunctions
internal sealed class RejectBody
{
public string? Reason { get; set; }
public string? Reason { get; set; }
public string? RejectedBy { get; set; }
}
internal sealed class CompleteBody
{
public string? PlatformClientId { get; set; }
}
}