Revised Registration
All checks were successful
Registration / build-deploy (push) Successful in 9m8s
All checks were successful
Registration / build-deploy (push) Successful in 9m8s
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user