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.Data;
using System.Security.Claims;
using System.Text.Json;
namespace Registration.Functions;
///
/// Registration HTTP endpoints.
///
/// ENDPOINTS:
/// POST /api/registration/register — Public; requires Bearer token (CIAM JWT).
/// 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/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
/// ═══════════════════════════════════════════════════════
///
// ── 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 _log;
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
public RegistrationFunctions(IRegistrationDataService data, ILogger log)
{
_data = data;
_log = log;
}
// ── Helpers ──────────────────────────────────────────────────────────
///
/// Extract the Entra Object ID from a validated CIAM JWT.
/// The OID claim is stable across sessions and providers (Google, Apple, Microsoft).
///
private static string? GetEntraSubjectId(ClaimsPrincipal user) =>
user.FindFirst("oid")?.Value
?? user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value
?? user.FindFirst("sub")?.Value;
// ── Public: Register ─────────────────────────────────────────────────
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Register")]
[Authorize]
public async Task Register(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/register")] HttpRequest req,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpPost("registration/register")]
// [Authorize]
// public async Task Register(CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
var entraSubjectId = GetEntraSubjectId(req.HttpContext.User);
if (string.IsNullOrEmpty(entraSubjectId))
{
_log.LogWarning("[Registration] Register called without a valid token OID claim");
return new UnauthorizedObjectResult(new { ok = false, error = "Valid authentication required" });
}
RegisterRequest? request;
try
{
request = await JsonSerializer.DeserializeAsync(req.Body, JsonOpts, ct);
}
catch
{
return new BadRequestObjectResult(new { ok = false, error = "Invalid JSON body" });
}
if (request == null || string.IsNullOrWhiteSpace(request.BusinessName))
return new BadRequestObjectResult(new { ok = false, error = "businessName is required" });
request.EntraSubjectId = entraSubjectId;
_log.LogInformation("[Registration] POST register: {Name} by entra={EntraId}",
request.BusinessName, entraSubjectId);
var result = await _data.RegisterAsync(request, ct);
if (!result.Ok)
return new BadRequestObjectResult(result);
return new OkObjectResult(result);
}
// ── Admin: List pending ───────────────────────────────────────────────
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("GetPending")]
public async Task GetPending(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/pending")] HttpRequest req,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpGet("registration/pending")]
// public async Task GetPending(CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
_log.LogInformation("[Registration] GET pending");
var result = await _data.GetPendingAsync(ct);
return new OkObjectResult(result);
}
// ── Admin: Get by ID ─────────────────────────────────────────────────
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("GetById")]
public async Task GetById(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/item/{registrationId}")] HttpRequest req,
string registrationId,
CancellationToken ct)
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
// [HttpGet("registration/item/{registrationId}")]
// public async Task GetById(string registrationId, CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
_log.LogInformation("[Registration] GET {Id}", registrationId);
var applicant = await _data.GetByIdAsync(registrationId, ct);
if (applicant == null)
return new NotFoundObjectResult(new { ok = false, error = "Registration not found" });
return new OkObjectResult(new { ok = true, applicant });
}
// ── Admin: Reject ────────────────────────────────────────────────────
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Reject")]
public async Task 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 Reject(string registrationId, CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
RejectBody? body = null;
try { body = await JsonSerializer.DeserializeAsync(req.Body, JsonOpts, ct); }
catch { /* optional body */ }
_log.LogInformation("[Registration] POST reject: {Id} reason={Reason}",
registrationId, body?.Reason);
var result = await _data.RejectAsync(registrationId, body?.Reason, body?.RejectedBy, ct);
if (!result.Ok)
return new BadRequestObjectResult(result);
return new OkObjectResult(result);
}
// ── Admin: Complete ───────────────────────────────────────────────────
// ── SWAP: Azure Functions ─────────────────────────────────────────────
[Function("Complete")]
public async Task 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 Complete(string registrationId, CancellationToken ct)
// ─────────────────────────────────────────────────────────────────────
{
// AspNetCore only — uncomment when in AspNetCore mode:
// var req = HttpContext.Request;
CompleteBody? body = null;
try { body = await JsonSerializer.DeserializeAsync(req.Body, JsonOpts, ct); }
catch { /* optional body */ }
_log.LogInformation("[Registration] POST complete: {Id} → platform client {PlatformId}",
registrationId, body?.PlatformClientId);
var result = await _data.CompleteAsync(registrationId, body?.PlatformClientId, ct);
if (!result.Ok)
return new BadRequestObjectResult(result);
return new OkObjectResult(result);
}
// ── Health ───────────────────────────────────────────────────────────
// ── 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
{
ok = true,
service = "registration",
mode = _data is Registration.Mock.MockDataService ? "mock" : "database",
timestamp = DateTime.UtcNow
});
}
}
// ── Request / Response Bodies ─────────────────────────────────────────────
internal sealed class RejectBody
{
public string? Reason { get; set; }
public string? RejectedBy { get; set; }
}
internal sealed class CompleteBody
{
public string? PlatformClientId { get; set; }
}