using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker; 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 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/health — Anonymous. /// public class RegistrationFunctions { 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 ───────────────────────────────────────────────── /// /// 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. /// [Function("Register")] [Authorize] public async Task Register( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/register")] HttpRequest req, CancellationToken ct) { // Identity comes from the validated JWT — never from the request body 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" }); // Stamp the server-validated identity — client-supplied value is ignored 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 ─────────────────────────────────────────────── /// List all pending registrations. Called by Management API with Function key. [Function("GetPending")] public async Task GetPending( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/pending")] HttpRequest req, CancellationToken ct) { _log.LogInformation("[Registration] GET pending"); var result = await _data.GetPendingAsync(ct); return new OkObjectResult(result); } // ── Admin: Get by ID ───────────────────────────────────────────────── /// Get a single applicant by registration ID. Called by Management API. [Function("GetById")] public async Task GetById( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/item/{registrationId}")] HttpRequest req, 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 ──────────────────────────────────────────────────── /// Reject a pending applicant. Called by Management after admin clicks Reject. [Function("Reject")] public async Task Reject( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/reject")] HttpRequest req, string registrationId, CancellationToken ct) { 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 (approved) ──────────────────────────────────────── /// /// Mark a registration as approved/completed. /// Called by Management after spClientManagement.create succeeds. /// Receives the platformClientId to link the registration to the platform record. /// [Function("Complete")] public async Task Complete( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/complete")] HttpRequest req, string registrationId, CancellationToken ct) { 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 ─────────────────────────────────────────────────────────── /// Health check — anonymous, no auth required. [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", 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; } }