Compare commits
8 Commits
37b08ef012
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce747ce48 | ||
|
|
b4fd0b6c9e | ||
|
|
0e17da63d0 | ||
|
|
a6df344e80 | ||
|
|
fae2226581 | ||
|
|
8de463cd17 | ||
|
|
866ab983c5 | ||
|
|
44764bc641 |
@@ -13,6 +13,28 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
|
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
|
||||||
builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
|
builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
|
||||||
|
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// CORS — allowed origins from env var, comma-separated
|
||||||
|
// --------------------
|
||||||
|
var allowedOrigins = (builder.Configuration["CORS__AllowedOrigins"] ?? "")
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddDefaultPolicy(policy =>
|
||||||
|
{
|
||||||
|
if (allowedOrigins.Length > 0)
|
||||||
|
policy.WithOrigins(allowedOrigins)
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
else
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
// Services
|
// Services
|
||||||
// --------------------
|
// --------------------
|
||||||
@@ -174,6 +196,9 @@ app.MapGet("/", () => Results.Ok(new
|
|||||||
status = "Application Gateway running"
|
status = "Application Gateway running"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// CORS — must be before auth middleware
|
||||||
|
app.UseCors();
|
||||||
|
|
||||||
// Access logging middleware (captures all requests)
|
// Access logging middleware (captures all requests)
|
||||||
// Placed BEFORE auth so we log even failed auth attempts
|
// Placed BEFORE auth so we log even failed auth attempts
|
||||||
app.UseAccessLogging();
|
app.UseAccessLogging();
|
||||||
|
|||||||
@@ -250,8 +250,8 @@ public sealed class MultiProviderAuthMiddleware
|
|||||||
{
|
{
|
||||||
// Standard Entra ID — could be CIAM tenant or Staff tenant (Tech, Admin)
|
// Standard Entra ID — could be CIAM tenant or Staff tenant (Tech, Admin)
|
||||||
// Detect by comparing issuer against configured Staff tenant ID
|
// Detect by comparing issuer against configured Staff tenant ID
|
||||||
var staffTenantId = _config["Auth:Microsoft:StaffTenantId"];
|
var staffTenantId = _config["Auth:Staff:TenantId"];
|
||||||
var staffClientId = _config["Auth:Microsoft:StaffClientId"];
|
var staffClientId = _config["Auth:Staff:ClientId"];
|
||||||
|
|
||||||
var isStaff = !string.IsNullOrWhiteSpace(staffTenantId) &&
|
var isStaff = !string.IsNullOrWhiteSpace(staffTenantId) &&
|
||||||
jwt.Issuer.Contains(staffTenantId, StringComparison.OrdinalIgnoreCase);
|
jwt.Issuer.Contains(staffTenantId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class ImageStorageService
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_blobClient = blobClient;
|
_blobClient = blobClient;
|
||||||
_containerName = config["BlobStorage:ContainerName"] ?? "creative-images";
|
_containerName = config["BlobStorage:ContainerName"] ?? "creative-images";
|
||||||
_blobBaseUrl = config["BlobStorage:BaseUrl"] ?? "https://usimadpcreatives.blob.core.windows.net";
|
_blobBaseUrl = config["BlobStorage:BaseUrl"] ?? string.Empty;
|
||||||
_isConfigured = blobClient != null;
|
_isConfigured = blobClient != null;
|
||||||
|
|
||||||
if (!_isConfigured)
|
if (!_isConfigured)
|
||||||
|
|||||||
@@ -6,41 +6,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"AllowDevBypass": false,
|
"AllowDevBypass": false,
|
||||||
|
|
||||||
"Microsoft": {
|
"Microsoft": {
|
||||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e",
|
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
||||||
"StaffTenantId": "0be4c23a-6941-4bdb-b397-a4faf88de4b3",
|
|
||||||
"StaffClientId": "b0f29246-91e7-4615-96db-5de9b6f8da2e"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"EntraId": {
|
"EntraId": {
|
||||||
"Instance": "https://login.microsoftonline.com/",
|
"Instance": "https://PositiveSpendClients.ciamlogin.com/",
|
||||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"BlobStorage": {
|
"BlobStorage": {
|
||||||
"ConnectionString": "",
|
"ConnectionString": "",
|
||||||
"ContainerName": "creative-images",
|
"ContainerName": "creative-images",
|
||||||
"BaseUrl": "https://usimadpcreatives.blob.core.windows.net"
|
"BaseUrl": ""
|
||||||
},
|
},
|
||||||
|
|
||||||
"MultiChannel": {
|
"MultiChannel": {
|
||||||
"Allocation": {
|
"Allocation": {
|
||||||
"MinMultiChannelMonthlyBudget": 500.00,
|
"MinMultiChannelMonthlyBudget": 500.0,
|
||||||
"MaxChannelsPerInitiative": 5,
|
"MaxChannelsPerInitiative": 5,
|
||||||
"DefaultAllocationStrategy": "template",
|
"DefaultAllocationStrategy": "template",
|
||||||
"PerformanceEvalIntervalDays": 7,
|
"PerformanceEvalIntervalDays": 7,
|
||||||
"PerformanceLookbackDays": 14,
|
"PerformanceLookbackDays": 14,
|
||||||
"PerformanceLearningPeriodDays": 14,
|
"PerformanceLearningPeriodDays": 14,
|
||||||
"MaxAllocationShiftPct": 15.00,
|
"MaxAllocationShiftPct": 15.0,
|
||||||
"MinChannelAllocationPct": 10.00,
|
"MinChannelAllocationPct": 10.0,
|
||||||
"MaxChannelAllocationPct": 80.00
|
"MaxChannelAllocationPct": 80.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"AllowDevBypass": false,
|
"AllowDevBypass": false,
|
||||||
|
|
||||||
"Microsoft": {
|
"Microsoft": {
|
||||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Google": {
|
"Google": {
|
||||||
"ClientId": ""
|
"ClientId": ""
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -47,8 +47,23 @@
|
|||||||
* 3. Grant admin consent
|
* 3. Grant admin consent
|
||||||
* 4. Create a client secret → copy value → set Graph__ClientSecret env var
|
* 4. Create a client secret → copy value → set Graph__ClientSecret env var
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
* REGISTRATION API — called by RegistrationClient (typed HttpClient).
|
||||||
|
* Management proxies /api/registration/* to this service.
|
||||||
|
*
|
||||||
|
* BaseUrl: Registration ASP.NET Core container, proxied via nginx.
|
||||||
|
* Set via env var: Registration__BaseUrl
|
||||||
|
* FunctionKey: Shared secret validated by ApiKeyAuthFilter on admin endpoints.
|
||||||
|
* Set via env var: Registration__FunctionKey
|
||||||
|
* Must match Registration:FunctionKey on the RegServer.
|
||||||
|
*/
|
||||||
|
"Registration": {
|
||||||
|
"BaseUrl": "https://portal.positivespend.com/api",
|
||||||
|
"FunctionKey": ""
|
||||||
|
},
|
||||||
|
|
||||||
"Graph": {
|
"Graph": {
|
||||||
"TenantId": "0be4c23a-6941-4bdb-b397-a4faf88de4b3",
|
"TenantId": "f56a3c51-9b5c-4356-920f-b4dcf932a96b",
|
||||||
"ClientId": "b0f29246-91e7-4615-96db-5de9b6f8da2e",
|
"ClientId": "b0f29246-91e7-4615-96db-5de9b6f8da2e",
|
||||||
"ClientSecret": ""
|
"ClientSecret": ""
|
||||||
}
|
}
|
||||||
|
|||||||
55
Registration/Auth/ApiKeyAuthFilter.cs
Normal file
55
Registration/Auth/ApiKeyAuthFilter.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Registration.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the x-functions-key header on admin endpoints.
|
||||||
|
///
|
||||||
|
/// In Azure Functions mode, admin endpoints used AuthorizationLevel.Function —
|
||||||
|
/// the Functions host validated the key automatically. In ASP.NET Core mode
|
||||||
|
/// that host doesn't exist, so this filter replicates the same behaviour.
|
||||||
|
///
|
||||||
|
/// The same header name (x-functions-key) and the same env var
|
||||||
|
/// (Registration__FunctionKey) are used in both modes, so the Management API
|
||||||
|
/// and Gateway require zero changes when the host is swapped.
|
||||||
|
///
|
||||||
|
/// Key configuration (docker-compose .env already has this):
|
||||||
|
/// Registration__FunctionKey=mra0B2boC5m36E7CUn-Urhwp7k3t3QvPZKjJvtNVEdVgAzFuuaAyRA==
|
||||||
|
///
|
||||||
|
/// ── SWAP note ─────────────────────────────────────────────────────────────
|
||||||
|
/// This file is only compiled and used in ASP.NET Core mode.
|
||||||
|
/// When restoring Azure Functions mode, the [ApiKeyAuth] attributes on admin
|
||||||
|
/// endpoints in RegistrationFunctions.cs are replaced by AuthorizationLevel.Function
|
||||||
|
/// in the [HttpTrigger] attributes, and this filter becomes unused (but harmless).
|
||||||
|
/// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||||
|
public sealed class ApiKeyAuthAttribute : Attribute, IResourceFilter
|
||||||
|
{
|
||||||
|
private const string HeaderName = "x-functions-key";
|
||||||
|
|
||||||
|
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||||
|
{
|
||||||
|
var config = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
|
||||||
|
var expected = config["Registration:FunctionKey"];
|
||||||
|
|
||||||
|
// If no key is configured, block all admin traffic — fail secure.
|
||||||
|
if (string.IsNullOrWhiteSpace(expected))
|
||||||
|
{
|
||||||
|
context.Result = new ObjectResult(new { ok = false, error = "Admin API key not configured on server" })
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status503ServiceUnavailable
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderName, out var provided)
|
||||||
|
|| !string.Equals(expected, provided, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
context.Result = new UnauthorizedObjectResult(new { ok = false, error = "Invalid or missing API key" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnResourceExecuted(ResourceExecutedContext context) { }
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ public sealed class Applicant
|
|||||||
public string? WebsiteUrl { get; set; }
|
public string? WebsiteUrl { get; set; }
|
||||||
public string? BusinessCategory { get; set; }
|
public string? BusinessCategory { get; set; }
|
||||||
public string? BusinessDescription { get; set; }
|
public string? BusinessDescription { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
public string? ContactName { get; set; }
|
public string? ContactName { get; set; }
|
||||||
public string? ContactEmail { get; set; }
|
public string? ContactEmail { get; set; }
|
||||||
public string? ContactPhone { get; set; }
|
public string? ContactPhone { get; set; }
|
||||||
@@ -42,7 +44,9 @@ public sealed class RegisterRequest
|
|||||||
public string? WebsiteUrl { get; set; }
|
public string? WebsiteUrl { get; set; }
|
||||||
public string? BusinessCategory { get; set; }
|
public string? BusinessCategory { get; set; }
|
||||||
public string? BusinessDescription { get; set; }
|
public string? BusinessDescription { get; set; }
|
||||||
public string? ContactName { get; set; }
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public string? ContactName { get; set; } // Combined first + last, set by client
|
||||||
public string? ContactEmail { get; set; }
|
public string? ContactEmail { get; set; }
|
||||||
public string? ContactPhone { get; set; }
|
public string? ContactPhone { get; set; }
|
||||||
public string? EntraSubjectId { get; set; }
|
public string? EntraSubjectId { get; set; }
|
||||||
|
|||||||
@@ -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.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Azure.Functions.Worker; // ← SWAP: remove for AspNetCore
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Registration.Auth;
|
||||||
using Registration.Data;
|
using Registration.Data;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
// ── SWAP: Azure Functions — uncomment this using when restoring Functions mode
|
||||||
|
// using Microsoft.Azure.Functions.Worker;
|
||||||
|
|
||||||
namespace Registration.Functions;
|
namespace Registration.Functions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -17,10 +26,10 @@ namespace Registration.Functions;
|
|||||||
/// entraSubjectId is extracted from the validated
|
/// entraSubjectId is extracted from the validated
|
||||||
/// token — the client never supplies it.
|
/// token — the client never supplies it.
|
||||||
///
|
///
|
||||||
/// GET /api/registration/pending — Admin; requires internal API key.
|
/// GET /api/registration/pending — Admin; x-functions-key header required.
|
||||||
/// GET /api/registration/item/{id} — Admin; requires internal API key.
|
/// GET /api/registration/item/{id} — Admin; x-functions-key header required.
|
||||||
/// POST /api/registration/action/{id}/reject — Admin; requires internal API key.
|
/// POST /api/registration/action/{id}/reject — Admin; x-functions-key header required.
|
||||||
/// POST /api/registration/action/{id}/complete — Admin; requires internal API key.
|
/// POST /api/registration/action/{id}/complete — Admin; x-functions-key header required.
|
||||||
/// GET /api/health — Anonymous.
|
/// GET /api/health — Anonymous.
|
||||||
///
|
///
|
||||||
/// ═══════════════════════════════════════════════════════
|
/// ═══════════════════════════════════════════════════════
|
||||||
@@ -29,20 +38,23 @@ namespace Registration.Functions;
|
|||||||
/// 2. Program.cs
|
/// 2. Program.cs
|
||||||
/// 3. Registration.csproj
|
/// 3. Registration.csproj
|
||||||
///
|
///
|
||||||
/// To switch to ASP.NET Core:
|
/// To switch back to Azure Functions:
|
||||||
/// a) In this file: swap class declaration and method signatures (marked below)
|
/// a) Swap class declaration (marked below)
|
||||||
/// b) In Program.cs: comment Azure Functions block, uncomment AspNetCore block
|
/// b) Swap each method signature (marked below)
|
||||||
/// c) In Registration.csproj: swap ItemGroup (marked in that file)
|
/// c) Uncomment [Function(...)] and [HttpTrigger(...)] attributes
|
||||||
/// d) In authConfig.js (client): update API_BASE_URL, remove API_FUNCTION_KEY
|
/// 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>
|
/// </summary>
|
||||||
|
|
||||||
// ── SWAP: Azure Functions class declaration ───────────────────────────────
|
// ── SWAP: ASP.NET Core class declaration ◄ ACTIVE ───────────────────────
|
||||||
public class RegistrationFunctions
|
[ApiController]
|
||||||
// ── SWAP: AspNetCore class declaration ────────────────────────────────────
|
[Route("api")]
|
||||||
// [ApiController]
|
public class RegistrationFunctions : ControllerBase
|
||||||
// [Route("api")]
|
// ── SWAP: Azure Functions class declaration ◄ INACTIVE — uncomment to restore
|
||||||
// public class RegistrationFunctions : ControllerBase
|
// public class RegistrationFunctions
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
private readonly IRegistrationDataService _data;
|
private readonly IRegistrationDataService _data;
|
||||||
@@ -66,6 +78,7 @@ public class RegistrationFunctions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extract the Entra Object ID from a validated CIAM JWT.
|
/// Extract the Entra Object ID from a validated CIAM JWT.
|
||||||
/// The OID claim is stable across sessions and providers (Google, Apple, Microsoft).
|
/// The OID claim is stable across sessions and providers (Google, Apple, Microsoft).
|
||||||
|
/// CIAM tenant: PositiveSpendClients.ciamlogin.com / cbf8b7d7-1e13-486d-b5b0-287ba79fdf0b
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string? GetEntraSubjectId(ClaimsPrincipal user) =>
|
private static string? GetEntraSubjectId(ClaimsPrincipal user) =>
|
||||||
user.FindFirst("oid")?.Value
|
user.FindFirst("oid")?.Value
|
||||||
@@ -74,20 +87,21 @@ public class RegistrationFunctions
|
|||||||
|
|
||||||
// ── Public: Register ─────────────────────────────────────────────────
|
// ── Public: Register ─────────────────────────────────────────────────
|
||||||
|
|
||||||
// ── SWAP: Azure Functions ─────────────────────────────────────────────
|
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
|
||||||
[Function("Register")]
|
[HttpPost("registration/register")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> Register(
|
public async Task<IActionResult> Register(CancellationToken ct)
|
||||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/register")] HttpRequest req,
|
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
|
||||||
CancellationToken ct)
|
// [Function("Register")]
|
||||||
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
|
|
||||||
// [HttpPost("registration/register")]
|
|
||||||
// [Authorize]
|
// [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:
|
// ── SWAP: ASP.NET Core uses HttpContext.Request ───────────────────
|
||||||
// var req = HttpContext.Request;
|
var req = HttpContext.Request;
|
||||||
|
// ── SWAP: Azure Functions — req is passed as parameter, remove above line
|
||||||
|
|
||||||
var entraSubjectId = GetEntraSubjectId(req.HttpContext.User);
|
var entraSubjectId = GetEntraSubjectId(req.HttpContext.User);
|
||||||
|
|
||||||
@@ -110,6 +124,7 @@ public class RegistrationFunctions
|
|||||||
if (request == null || string.IsNullOrWhiteSpace(request.BusinessName))
|
if (request == null || string.IsNullOrWhiteSpace(request.BusinessName))
|
||||||
return new BadRequestObjectResult(new { ok = false, error = "businessName is required" });
|
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;
|
request.EntraSubjectId = entraSubjectId;
|
||||||
|
|
||||||
_log.LogInformation("[Registration] POST register: {Name} by entra={EntraId}",
|
_log.LogInformation("[Registration] POST register: {Name} by entra={EntraId}",
|
||||||
@@ -125,14 +140,15 @@ public class RegistrationFunctions
|
|||||||
|
|
||||||
// ── Admin: List pending ───────────────────────────────────────────────
|
// ── Admin: List pending ───────────────────────────────────────────────
|
||||||
|
|
||||||
// ── SWAP: Azure Functions ─────────────────────────────────────────────
|
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
|
||||||
[Function("GetPending")]
|
[HttpGet("registration/pending")]
|
||||||
public async Task<IActionResult> GetPending(
|
[ApiKeyAuth]
|
||||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/pending")] HttpRequest req,
|
public async Task<IActionResult> GetPending(CancellationToken ct)
|
||||||
CancellationToken ct)
|
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
|
||||||
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
|
// [Function("GetPending")]
|
||||||
// [HttpGet("registration/pending")]
|
// public async Task<IActionResult> GetPending(
|
||||||
// public async Task<IActionResult> GetPending(CancellationToken ct)
|
// [HttpTrigger(AuthorizationLevel.Function, "get", Route = "registration/pending")] HttpRequest req,
|
||||||
|
// CancellationToken ct)
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
_log.LogInformation("[Registration] GET pending");
|
_log.LogInformation("[Registration] GET pending");
|
||||||
@@ -142,15 +158,16 @@ public class RegistrationFunctions
|
|||||||
|
|
||||||
// ── Admin: Get by ID ─────────────────────────────────────────────────
|
// ── Admin: Get by ID ─────────────────────────────────────────────────
|
||||||
|
|
||||||
// ── SWAP: Azure Functions ─────────────────────────────────────────────
|
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
|
||||||
[Function("GetById")]
|
[HttpGet("registration/item/{registrationId}")]
|
||||||
public async Task<IActionResult> GetById(
|
[ApiKeyAuth]
|
||||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/item/{registrationId}")] HttpRequest req,
|
public async Task<IActionResult> GetById(string registrationId, CancellationToken ct)
|
||||||
string registrationId,
|
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
|
||||||
CancellationToken ct)
|
// [Function("GetById")]
|
||||||
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
|
// public async Task<IActionResult> GetById(
|
||||||
// [HttpGet("registration/item/{registrationId}")]
|
// [HttpTrigger(AuthorizationLevel.Function, "get", Route = "registration/item/{registrationId}")] HttpRequest req,
|
||||||
// public async Task<IActionResult> GetById(string registrationId, CancellationToken ct)
|
// string registrationId,
|
||||||
|
// CancellationToken ct)
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
_log.LogInformation("[Registration] GET {Id}", registrationId);
|
_log.LogInformation("[Registration] GET {Id}", registrationId);
|
||||||
@@ -164,19 +181,21 @@ public class RegistrationFunctions
|
|||||||
|
|
||||||
// ── Admin: Reject ────────────────────────────────────────────────────
|
// ── Admin: Reject ────────────────────────────────────────────────────
|
||||||
|
|
||||||
// ── SWAP: Azure Functions ─────────────────────────────────────────────
|
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
|
||||||
[Function("Reject")]
|
[HttpPost("registration/action/{registrationId}/reject")]
|
||||||
public async Task<IActionResult> Reject(
|
[ApiKeyAuth]
|
||||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/reject")] HttpRequest req,
|
public async Task<IActionResult> Reject(string registrationId, CancellationToken ct)
|
||||||
string registrationId,
|
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
|
||||||
CancellationToken ct)
|
// [Function("Reject")]
|
||||||
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
|
// public async Task<IActionResult> Reject(
|
||||||
// [HttpPost("registration/action/{registrationId}/reject")]
|
// [HttpTrigger(AuthorizationLevel.Function, "post", Route = "registration/action/{registrationId}/reject")] HttpRequest req,
|
||||||
// public async Task<IActionResult> Reject(string registrationId, CancellationToken ct)
|
// string registrationId,
|
||||||
|
// CancellationToken ct)
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
// AspNetCore only — uncomment when in AspNetCore mode:
|
// ── SWAP: ASP.NET Core ────────────────────────────────────────────
|
||||||
// var req = HttpContext.Request;
|
var req = HttpContext.Request;
|
||||||
|
// ── SWAP: Azure Functions — req is passed as parameter, remove above line
|
||||||
|
|
||||||
RejectBody? body = null;
|
RejectBody? body = null;
|
||||||
try { body = await JsonSerializer.DeserializeAsync<RejectBody>(req.Body, JsonOpts, ct); }
|
try { body = await JsonSerializer.DeserializeAsync<RejectBody>(req.Body, JsonOpts, ct); }
|
||||||
@@ -195,19 +214,21 @@ public class RegistrationFunctions
|
|||||||
|
|
||||||
// ── Admin: Complete ───────────────────────────────────────────────────
|
// ── Admin: Complete ───────────────────────────────────────────────────
|
||||||
|
|
||||||
// ── SWAP: Azure Functions ─────────────────────────────────────────────
|
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
|
||||||
[Function("Complete")]
|
[HttpPost("registration/action/{registrationId}/complete")]
|
||||||
public async Task<IActionResult> Complete(
|
[ApiKeyAuth]
|
||||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/complete")] HttpRequest req,
|
public async Task<IActionResult> Complete(string registrationId, CancellationToken ct)
|
||||||
string registrationId,
|
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
|
||||||
CancellationToken ct)
|
// [Function("Complete")]
|
||||||
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
|
// public async Task<IActionResult> Complete(
|
||||||
// [HttpPost("registration/action/{registrationId}/complete")]
|
// [HttpTrigger(AuthorizationLevel.Function, "post", Route = "registration/action/{registrationId}/complete")] HttpRequest req,
|
||||||
// public async Task<IActionResult> Complete(string registrationId, CancellationToken ct)
|
// string registrationId,
|
||||||
|
// CancellationToken ct)
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
// AspNetCore only — uncomment when in AspNetCore mode:
|
// ── SWAP: ASP.NET Core ────────────────────────────────────────────
|
||||||
// var req = HttpContext.Request;
|
var req = HttpContext.Request;
|
||||||
|
// ── SWAP: Azure Functions — req is passed as parameter, remove above line
|
||||||
|
|
||||||
CompleteBody? body = null;
|
CompleteBody? body = null;
|
||||||
try { body = await JsonSerializer.DeserializeAsync<CompleteBody>(req.Body, JsonOpts, ct); }
|
try { body = await JsonSerializer.DeserializeAsync<CompleteBody>(req.Body, JsonOpts, ct); }
|
||||||
@@ -226,13 +247,14 @@ public class RegistrationFunctions
|
|||||||
|
|
||||||
// ── Health ───────────────────────────────────────────────────────────
|
// ── Health ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// ── SWAP: Azure Functions ─────────────────────────────────────────────
|
// ── SWAP: ASP.NET Core ◄ ACTIVE ─────────────────────────────────────
|
||||||
[Function("Health")]
|
[HttpGet("health")]
|
||||||
public IActionResult Health(
|
[AllowAnonymous]
|
||||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequest req)
|
public IActionResult Health()
|
||||||
// ── SWAP: AspNetCore ──────────────────────────────────────────────────
|
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
|
||||||
// [HttpGet("health")]
|
// [Function("Health")]
|
||||||
// public IActionResult Health()
|
// public IActionResult Health(
|
||||||
|
// [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequest req)
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
return new OkObjectResult(new
|
return new OkObjectResult(new
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
using Microsoft.Azure.Functions.Worker; // ← SWAP: remove for AspNetCore
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Identity.Web;
|
|
||||||
using Registration.Data;
|
|
||||||
using Registration.Mock;
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Shared service registrations — identical in both hosting modes.
|
// Shared service registrations — identical in both hosting modes.
|
||||||
// Never changes regardless of which block is active below.
|
// Never changes regardless of which block is active below.
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
using Microsoft.Identity.Web;
|
||||||
|
using Registration.Data;
|
||||||
|
|
||||||
void ConfigureServices(IServiceCollection services, IConfiguration config)
|
void ConfigureServices(IServiceCollection services, IConfiguration config)
|
||||||
{
|
{
|
||||||
// JWT authentication — Entra External ID (CIAM)
|
// JWT authentication — Entra External ID (CIAM)
|
||||||
// Validates Bearer tokens issued by usimclients.ciamlogin.com.
|
// Validates Bearer tokens issued by PositiveSpendClients.ciamlogin.com.
|
||||||
// AzureAd config comes from local.settings.json (Functions) or
|
// Config keys: AzureAd:Instance / TenantId / ClientId / Audience
|
||||||
// appsettings.json / environment variables (AspNetCore).
|
// In ASP.NET Core mode: appsettings.json + env vars (AzureAd__*)
|
||||||
|
// In Functions mode: local.settings.json AzureAd section (dev only)
|
||||||
services.AddAuthentication()
|
services.AddAuthentication()
|
||||||
.AddMicrosoftIdentityWebApi(config.GetSection("AzureAd"));
|
.AddMicrosoftIdentityWebApi(config.GetSection("AzureAd"));
|
||||||
|
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
|
|
||||||
// Data layer — swap between MockDataService (no DB) and SqlDataService (dbRegistration).
|
// Data layer — swap here to flip between mock (no DB) and real DB.
|
||||||
// MockDataService: no connection string required, seeds 4 test applicants.
|
// MockDataService: no connection string required, seeds 4 test applicants.
|
||||||
// SqlDataService: requires ConnectionStrings:Sql pointed at dbRegistration.
|
// SqlDataService: requires ConnectionStrings:Sql → dbRegistration on 10.10.99.212
|
||||||
// Calls dbo.spRegistration with @action/@rqst/@resp OUTPUT pattern.
|
// Calls dbo.spRegistration with @action/@rqst/@resp OUTPUT pattern.
|
||||||
services.AddSingleton<SqlService>();
|
services.AddSingleton<SqlService>();
|
||||||
services.AddSingleton<IRegistrationDataService, SqlDataService>();
|
services.AddSingleton<IRegistrationDataService, SqlDataService>();
|
||||||
@@ -32,45 +28,51 @@ void ConfigureServices(IServiceCollection services, IConfiguration config)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
// SWAP: Azure Functions host
|
// SWAP: ASP.NET Core host ◄ ACTIVE
|
||||||
|
// Running in docker-compose as registration:8080 behind nginx / Gateway.
|
||||||
|
// Dockerfile: mcr.microsoft.com/dotnet/aspnet:8.0 ← matches this mode.
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
ConfigureServices(builder.Services, builder.Configuration);
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// CORS — reads CORS:AllowedOrigins from appsettings.json or env var CORS__AllowedOrigins.
|
||||||
|
// Comma-separated list. Matches the value already in docker-compose .env:
|
||||||
|
// CORS__AllowedOrigins=https://client.positivespend.com,https://portal.positivespend.com,...
|
||||||
|
builder.Services.AddCors(options => options.AddDefaultPolicy(policy =>
|
||||||
|
policy.WithOrigins(
|
||||||
|
(builder.Configuration["CORS:AllowedOrigins"] ?? "")
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
)
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()));
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseCors();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// SWAP: Azure Functions host ◄ INACTIVE — uncomment to restore
|
||||||
// Active when deployed to Azure Functions or running via: func start
|
// Active when deployed to Azure Functions or running via: func start
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
|
||||||
var host = new HostBuilder()
|
|
||||||
.ConfigureFunctionsWebApplication()
|
|
||||||
.ConfigureServices((ctx, services) =>
|
|
||||||
{
|
|
||||||
services.AddApplicationInsightsTelemetryWorkerService();
|
|
||||||
services.ConfigureFunctionsApplicationInsights();
|
|
||||||
ConfigureServices(services, ctx.Configuration);
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
host.Run();
|
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
|
||||||
// SWAP: ASP.NET Core host
|
|
||||||
// Active when running in podman-compose or dotnet run (self-hosted).
|
|
||||||
// Also update RegistrationFunctions.cs and Registration.csproj.
|
// Also update RegistrationFunctions.cs and Registration.csproj.
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
// var builder = WebApplication.CreateBuilder(args);
|
// using Microsoft.Azure.Functions.Worker;
|
||||||
//
|
//
|
||||||
// ConfigureServices(builder.Services, builder.Configuration);
|
// var host = new HostBuilder()
|
||||||
// builder.Services.AddControllers();
|
// .ConfigureFunctionsWebApplication()
|
||||||
|
// .ConfigureServices((ctx, services) =>
|
||||||
|
// {
|
||||||
|
// services.AddApplicationInsightsTelemetryWorkerService();
|
||||||
|
// services.ConfigureFunctionsApplicationInsights();
|
||||||
|
// ConfigureServices(services, ctx.Configuration);
|
||||||
|
// })
|
||||||
|
// .Build();
|
||||||
//
|
//
|
||||||
// // CORS — replace with actual client origin in production
|
// host.Run();
|
||||||
// builder.Services.AddCors(options => options.AddDefaultPolicy(policy =>
|
|
||||||
// policy.WithOrigins(
|
|
||||||
// builder.Configuration["Cors:AllowedOrigin"] ?? "http://localhost:3001"
|
|
||||||
// )
|
|
||||||
// .AllowAnyHeader()
|
|
||||||
// .AllowAnyMethod()));
|
|
||||||
//
|
|
||||||
// var app = builder.Build();
|
|
||||||
//
|
|
||||||
// app.UseCors();
|
|
||||||
// app.UseAuthentication();
|
|
||||||
// app.UseAuthorization();
|
|
||||||
// app.MapControllers();
|
|
||||||
//
|
|
||||||
// await app.RunAsync();
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Microsoft.Resources/deployments",
|
"type": "Microsoft.Resources/deployments",
|
||||||
"name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('usim-adp-registration', subscription().subscriptionId)))]",
|
"name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('ps-adp-registration', subscription().subscriptionId)))]",
|
||||||
"resourceGroup": "[parameters('resourceGroupName')]",
|
"resourceGroup": "[parameters('resourceGroupName')]",
|
||||||
"apiVersion": "2019-10-01",
|
"apiVersion": "2019-10-01",
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"kind": "web",
|
"kind": "web",
|
||||||
"name": "usim-adp-registration",
|
"name": "ps-adp-registration",
|
||||||
"type": "microsoft.insights/components",
|
"type": "microsoft.insights/components",
|
||||||
"location": "[parameters('resourceLocation')]",
|
"location": "[parameters('resourceLocation')]",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"resourceName": {
|
"resourceName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"defaultValue": "usim-adp-registration",
|
"defaultValue": "ps-adp-registration",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Name of the main resource to be created by this template."
|
"description": "Name of the main resource to be created by this template."
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"connectionId": "AzureWebJobsStorage"
|
"connectionId": "AzureWebJobsStorage"
|
||||||
},
|
},
|
||||||
"appInsights1": {
|
"appInsights1": {
|
||||||
"resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/microsoft.insights/components/usim-adp-registration",
|
"resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/microsoft.insights/components/ps-adp-registration",
|
||||||
"type": "appInsights.azure",
|
"type": "appInsights.azure",
|
||||||
"connectionId": "APPLICATIONINSIGHTS_CONNECTION_STRING"
|
"connectionId": "APPLICATIONINSIGHTS_CONNECTION_STRING"
|
||||||
}
|
}
|
||||||
@@ -1,91 +1,126 @@
|
|||||||
# Registration Function
|
# Registration Service
|
||||||
|
|
||||||
Azure Function (isolated worker, .NET 8) for managing prospect registration in AdPlatform.
|
ASP.NET Core (.NET 8) service for managing prospect registration in AdPlatform.
|
||||||
|
Self-hosted via docker-compose as `registration:8080` behind nginx / Gateway.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
Prospect → Registration Function → dbRegistration (future)
|
Client SPA (client.positivespend.com)
|
||||||
Admin Panel → Management API → Registration Function (proxy)
|
└─► Gateway (portal.positivespend.com)
|
||||||
→ spClientManagement (approve → dbAdPlatform)
|
└─► Registration API (regapi.positivespend.com → registration:8080)
|
||||||
|
├─► dbRegistration (10.10.99.212 — Registrations table, spRegistration)
|
||||||
|
└─► CIAM (PositiveSpendClients.ciamlogin.com — token validation)
|
||||||
|
|
||||||
|
Management API (mgmt.positivespend.com)
|
||||||
|
└─► Registration API (admin endpoints, x-functions-key auth)
|
||||||
|
└─► dbRegistration
|
||||||
```
|
```
|
||||||
|
|
||||||
Management validates admin sessions, then proxies registration calls to this Function.
|
The Registration service never touches `dbAdPlatform`. Management never touches `dbRegistration`.
|
||||||
The Function never touches `dbAdPlatform`. Management never touches `dbRegistration`.
|
Approval provisioning into `dbAdPlatform` is the Management API's responsibility, called after
|
||||||
|
the `complete` action marks a registration as Approved.
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
| Method | Route | Auth | Description |
|
| Method | Route | Auth | Description |
|
||||||
|--------|-------|------|-------------|
|
|--------|-------|------|-------------|
|
||||||
| GET | `/api/registration/pending` | Function Key | List pending applicants |
|
| `POST` | `/api/registration/register` | Bearer (CIAM JWT) | New prospect signup |
|
||||||
| GET | `/api/registration/{id}` | Function Key | Get single applicant |
|
| `GET` | `/api/registration/pending` | x-functions-key | List pending applicants |
|
||||||
| POST | `/api/registration/register` | Function Key | New prospect signup |
|
| `GET` | `/api/registration/item/{id}` | x-functions-key | Get single applicant |
|
||||||
| POST | `/api/registration/{id}/reject` | Function Key | Reject applicant |
|
| `POST` | `/api/registration/action/{id}/reject` | x-functions-key | Reject applicant |
|
||||||
| POST | `/api/registration/{id}/complete` | Function Key | Mark approved (called after platform client created) |
|
| `POST` | `/api/registration/action/{id}/complete` | x-functions-key | Mark approved |
|
||||||
| GET | `/api/registration/health` | Anonymous | Health check |
|
| `GET` | `/api/health` | Anonymous | Health check |
|
||||||
|
|
||||||
## Mock Mode (Current)
|
`entraSubjectId` is **never** read from the request body on `/register` — it is extracted
|
||||||
|
from the validated CIAM Bearer token (OID claim). The client cannot spoof it.
|
||||||
|
|
||||||
Starts with 4 realistic test applicants in memory. State persists within a Function host
|
## Authentication
|
||||||
lifecycle and resets on cold start. No database required.
|
|
||||||
|
**Client-facing endpoint** (`/register`): Bearer token issued by
|
||||||
|
`PositiveSpendClients.ciamlogin.com` (tenant `cbf8b7d7`). Validated by
|
||||||
|
`Microsoft.Identity.Web` against the CIAM tenant. The Client SPA acquires this
|
||||||
|
token via MSAL after the user signs in with Google, Apple, or Microsoft.
|
||||||
|
|
||||||
|
**Admin endpoints** (`pending` / `item` / `reject` / `complete`): `x-functions-key` header
|
||||||
|
validated by `ApiKeyAuthFilter`. Key must match `Registration__FunctionKey` env var.
|
||||||
|
These endpoints are called by the Management API, not the browser.
|
||||||
|
|
||||||
|
## CIAM Tenant Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Tenant | Positive Spend Clients |
|
||||||
|
| Tenant ID | `cbf8b7d7-1e13-486d-b5b0-287ba79fdf0b` |
|
||||||
|
| Client SPA App ID | `c426967f-bfcc-46af-b4e5-d69dc01cbf75` |
|
||||||
|
| Authority | `https://PositiveSpendClients.ciamlogin.com/cbf8b7d7.../` |
|
||||||
|
|
||||||
|
This is the **client-facing CIAM tenant** — separate from the internal `positivespend.com`
|
||||||
|
org tenant (`f56a3c51`) used by Management and the Tech/Admin consoles.
|
||||||
|
|
||||||
|
## Mock Mode
|
||||||
|
|
||||||
|
Switch in `Program.cs` to run without a database (seeds 4 test applicants in memory):
|
||||||
|
|
||||||
To switch to mock mode, in `Program.cs`:
|
|
||||||
```csharp
|
```csharp
|
||||||
services.AddSingleton<IRegistrationDataService, MockDataService>();
|
services.AddSingleton<IRegistrationDataService, MockDataService>();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Database Mode (Future)
|
State resets on container restart — by design.
|
||||||
|
|
||||||
When `dbRegistration` is ready:
|
## Database Setup
|
||||||
|
|
||||||
1. Create the database and run the `spRegistration` stored proc migration
|
Run `dbo.spRegistration.sql` against `dbRegistration` on `10.10.99.212` once.
|
||||||
2. Set `ConnectionStrings:Sql` to the registration database connection string
|
It is idempotent (`CREATE OR ALTER PROCEDURE`, `IF OBJECT_ID ... IS NULL` guard on the table).
|
||||||
3. In `Program.cs`, swap DI registration:
|
|
||||||
```csharp
|
|
||||||
services.AddSingleton<SqlService>();
|
|
||||||
services.AddSingleton<IRegistrationDataService, SqlDataService>();
|
|
||||||
```
|
|
||||||
|
|
||||||
The `SqlDataService` calls `dbo.spRegistration` with the standard `@action/@rqst/@resp OUTPUT`
|
## docker-compose Environment Variables
|
||||||
pattern used across all AdPlatform services.
|
|
||||||
|
|
||||||
## Local Development
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Requires Azure Functions Core Tools
|
# SQL Server
|
||||||
|
ConnectionStrings__Sql=Server=10.10.99.212;Database=dbRegistration;User Id=appAdPlatformReg;Password=...;TrustServerCertificate=True;
|
||||||
|
|
||||||
|
# CIAM — already correct in .env
|
||||||
|
AzureAd__Instance=https://PositiveSpendClients.ciamlogin.com/
|
||||||
|
AzureAd__TenantId=cbf8b7d7-1e13-486d-b5b0-287ba79fdf0b
|
||||||
|
AzureAd__ClientId=c426967f-bfcc-46af-b4e5-d69dc01cbf75
|
||||||
|
|
||||||
|
# CORS — already in .env
|
||||||
|
CORS__AllowedOrigins=https://client.positivespend.com,https://portal.positivespend.com,...
|
||||||
|
|
||||||
|
# Admin key — already in .env
|
||||||
|
Registration__FunctionKey=mra0B2boC5m36E7CUn-Urhwp7k3t3QvPZKjJvtNVEdVgAzFuuaAyRA==
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://regapi.positivespend.com/api/health
|
||||||
|
# {"ok":true,"service":"registration","mode":"database","timestamp":"..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d regapi.positivespend.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Host Swap — Restore Azure Functions
|
||||||
|
|
||||||
|
Three files change. Everything else (Data layer, Mock layer, models, SQL) is identical.
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `Registration.csproj` | Comment ASP.NET Core `ItemGroup`, uncomment Functions `ItemGroup`, restore `<AzureFunctionsVersion>v4</AzureFunctionsVersion>`, change `Sdk` to `Microsoft.NET.Sdk` |
|
||||||
|
| `Program.cs` | Comment ASP.NET Core block, uncomment Functions `HostBuilder` block |
|
||||||
|
| `Functions/RegistrationFunctions.cs` | Swap class declaration, swap each method signature (all marked `◄ INACTIVE`) |
|
||||||
|
|
||||||
|
Client changes when restoring Functions:
|
||||||
|
- `authConfig.js`: set `API_BASE_URL` to the Azure Function App URL, set `API_FUNCTION_KEY` from Azure Portal → App Keys → default
|
||||||
|
|
||||||
|
Run locally in Functions mode:
|
||||||
|
```bash
|
||||||
func start
|
func start
|
||||||
|
curl http://localhost:7071/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Test with:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:7071/api/registration/health
|
|
||||||
curl http://localhost:7071/api/registration/pending
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
Deploy as an Azure Function App (Consumption or Flex Consumption plan).
|
|
||||||
|
|
||||||
After deployment:
|
|
||||||
1. Copy the Function Key from Azure Portal → Function App → App Keys
|
|
||||||
2. Set in Management API config:
|
|
||||||
- `Registration:BaseUrl` = `https://your-function-app.azurewebsites.net/api`
|
|
||||||
- `Registration:FunctionKey` = `<key from portal>`
|
|
||||||
|
|
||||||
These can be set as Azure Container App environment variables:
|
|
||||||
```
|
|
||||||
Registration__BaseUrl=https://your-function-app.azurewebsites.net/api
|
|
||||||
Registration__FunctionKey=<key>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mock Applicants
|
|
||||||
|
|
||||||
The mock data includes 4 test applicants representing the target market
|
|
||||||
(small businesses with low ad spend thresholds):
|
|
||||||
|
|
||||||
| Business | Category | Payment Verified | Days Waiting |
|
|
||||||
|----------|----------|-----------------|-------------|
|
|
||||||
| Bella's Boutique | Retail | Yes | 3 |
|
|
||||||
| Pacific Coast Plumbing | Home Services | Yes | 1 |
|
|
||||||
| Sunrise Dental Group | Healthcare | No | ~0.25 |
|
|
||||||
| FreshBite Meal Prep | Food & Beverage | Yes | ~0.08 |
|
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>Registration</RootNamespace>
|
<RootNamespace>Registration</RootNamespace>
|
||||||
<!-- SWAP: Azure Functions only — remove for AspNetCore -->
|
<!-- SWAP: AzureFunctionsVersion removed for ASP.NET Core mode. -->
|
||||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
<!-- To restore Azure Functions: add back <AzureFunctionsVersion>v4</AzureFunctionsVersion> -->
|
||||||
|
<!-- and change Sdk above to Microsoft.NET.Sdk -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
═══════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════
|
||||||
SWAP: Azure Functions packages (active)
|
SWAP: ASP.NET Core packages ◄ ACTIVE
|
||||||
To switch to AspNetCore: comment this ItemGroup, uncomment the one below,
|
Microsoft.NET.Sdk.Web pulls in Microsoft.AspNetCore.App automatically —
|
||||||
and remove <AzureFunctionsVersion> from PropertyGroup above.
|
no explicit FrameworkReference needed here.
|
||||||
═══════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════
|
||||||
-->
|
-->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||||
|
<PackageReference Include="Microsoft.Identity.Web" Version="4.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
═══════════════════════════════════════════════════════════════════════════
|
||||||
|
SWAP: Azure Functions packages ◄ INACTIVE — uncomment to restore
|
||||||
|
To switch back to Azure Functions:
|
||||||
|
1. Uncomment this ItemGroup
|
||||||
|
2. Comment the ASP.NET Core ItemGroup above
|
||||||
|
3. Restore <AzureFunctionsVersion>v4</AzureFunctionsVersion> in PropertyGroup
|
||||||
|
4. Change Sdk to Microsoft.NET.Sdk
|
||||||
|
5. Uncomment the Functions block in Program.cs
|
||||||
|
6. Swap class declaration and method signatures in RegistrationFunctions.cs
|
||||||
|
═══════════════════════════════════════════════════════════════════════════
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.51.0" />
|
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.51.0" />
|
||||||
@@ -28,23 +44,11 @@
|
|||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="4.5.0" />
|
<PackageReference Include="Microsoft.Identity.Web" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!--
|
|
||||||
═══════════════════════════════════════════════════════════════════════════
|
|
||||||
SWAP: ASP.NET Core packages
|
|
||||||
To switch to AspNetCore: uncomment this ItemGroup, comment the one above,
|
|
||||||
and remove <AzureFunctionsVersion> from PropertyGroup above.
|
|
||||||
═══════════════════════════════════════════════════════════════════════════
|
|
||||||
<ItemGroup>
|
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="4.5.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Azure Functions host files — present in both modes, ignored in AspNetCore.
|
host.json and local.settings.json are inert in ASP.NET Core mode.
|
||||||
host.json and local.settings.json are inert when running as a plain web app.
|
Kept in the repo so the Azure Functions SWAP path remains intact.
|
||||||
-->
|
-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="host.json">
|
<None Update="host.json">
|
||||||
|
|||||||
25
Registration/appsettings.json
Normal file
25
Registration/appsettings.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Sql": "Server=10.10.99.212;Database=dbRegistration;User Id=appAdPlatformReg;Password=REPLACE_ME;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
"AzureAd": {
|
||||||
|
"Instance": "https://positiveclients.ciamlogin.com/",
|
||||||
|
"TenantId": "cbf8b7d7-1e13-486d-b5b0-287ba79fdf0b",
|
||||||
|
"ClientId": "43c493e4-e1ed-4cd7-ab0a-e507e20af724",
|
||||||
|
"Audience": "43c493e4-e1ed-4cd7-ab0a-e507e20af724"
|
||||||
|
},
|
||||||
|
"CORS": {
|
||||||
|
"AllowedOrigins": "https://register.positivespend.com,https://client.positivespend.com,https://portal.positivespend.com"
|
||||||
|
},
|
||||||
|
"Registration": {
|
||||||
|
"FunctionKey": ""
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.Identity": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
"routePrefix": "api",
|
"routePrefix": "api",
|
||||||
"cors": {
|
"cors": {
|
||||||
"allowedOrigins": [
|
"allowedOrigins": [
|
||||||
"https://adpregist.usimdev.com",
|
"https://register.positivespend.com",
|
||||||
|
"https://client.positivespend.com",
|
||||||
"http://localhost:3001"
|
"http://localhost:3001"
|
||||||
],
|
],
|
||||||
"allowedHeaders": [ "*" ],
|
"allowedHeaders": [ "*" ],
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
|
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Sql": "Server=usimdev.database.windows.net;Database=dbRegistration;User Id=appAdPlatformReg;Password=YOUR_PASSWORD_HERE;TrustServerCertificate=True;"
|
"Sql": "Server=10.10.99.212;Database=dbRegistration;User Id=appAdPlatformReg;Password=REPLACE_ME;TrustServerCertificate=True;"
|
||||||
},
|
},
|
||||||
"AzureAd": {
|
"AzureAd": {
|
||||||
"Instance": "https://usimclients.ciamlogin.com/",
|
"Instance": "https://REPLACE_WITH_CIAM_SUBDOMAIN.ciamlogin.com/",
|
||||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
"TenantId": "REPLACE_WITH_CIAM_TENANT_ID",
|
||||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e",
|
"ClientId": "REPLACE_WITH_CIAM_CLIENT_ID",
|
||||||
"Audience": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
"Audience": "REPLACE_WITH_CIAM_CLIENT_ID"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user