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";
|
||||
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
|
||||
// --------------------
|
||||
@@ -174,6 +196,9 @@ app.MapGet("/", () => Results.Ok(new
|
||||
status = "Application Gateway running"
|
||||
}));
|
||||
|
||||
// CORS — must be before auth middleware
|
||||
app.UseCors();
|
||||
|
||||
// Access logging middleware (captures all requests)
|
||||
// Placed BEFORE auth so we log even failed auth attempts
|
||||
app.UseAccessLogging();
|
||||
|
||||
@@ -250,8 +250,8 @@ public sealed class MultiProviderAuthMiddleware
|
||||
{
|
||||
// Standard Entra ID — could be CIAM tenant or Staff tenant (Tech, Admin)
|
||||
// Detect by comparing issuer against configured Staff tenant ID
|
||||
var staffTenantId = _config["Auth:Microsoft:StaffTenantId"];
|
||||
var staffClientId = _config["Auth:Microsoft:StaffClientId"];
|
||||
var staffTenantId = _config["Auth:Staff:TenantId"];
|
||||
var staffClientId = _config["Auth:Staff:ClientId"];
|
||||
|
||||
var isStaff = !string.IsNullOrWhiteSpace(staffTenantId) &&
|
||||
jwt.Issuer.Contains(staffTenantId, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ImageStorageService
|
||||
_logger = logger;
|
||||
_blobClient = blobClient;
|
||||
_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;
|
||||
|
||||
if (!_isConfigured)
|
||||
|
||||
@@ -6,41 +6,34 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
|
||||
"Auth": {
|
||||
"AllowDevBypass": false,
|
||||
|
||||
"Microsoft": {
|
||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e",
|
||||
"StaffTenantId": "0be4c23a-6941-4bdb-b397-a4faf88de4b3",
|
||||
"StaffClientId": "b0f29246-91e7-4615-96db-5de9b6f8da2e"
|
||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
||||
},
|
||||
|
||||
"EntraId": {
|
||||
"Instance": "https://login.microsoftonline.com/",
|
||||
"Instance": "https://PositiveSpendClients.ciamlogin.com/",
|
||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
||||
}
|
||||
},
|
||||
|
||||
"BlobStorage": {
|
||||
"ConnectionString": "",
|
||||
"ContainerName": "creative-images",
|
||||
"BaseUrl": "https://usimadpcreatives.blob.core.windows.net"
|
||||
"BaseUrl": ""
|
||||
},
|
||||
|
||||
"MultiChannel": {
|
||||
"Allocation": {
|
||||
"MinMultiChannelMonthlyBudget": 500.00,
|
||||
"MinMultiChannelMonthlyBudget": 500.0,
|
||||
"MaxChannelsPerInitiative": 5,
|
||||
"DefaultAllocationStrategy": "template",
|
||||
"PerformanceEvalIntervalDays": 7,
|
||||
"PerformanceLookbackDays": 14,
|
||||
"PerformanceLearningPeriodDays": 14,
|
||||
"MaxAllocationShiftPct": 15.00,
|
||||
"MinChannelAllocationPct": 10.00,
|
||||
"MaxChannelAllocationPct": 80.00
|
||||
"MaxAllocationShiftPct": 15.0,
|
||||
"MinChannelAllocationPct": 10.0,
|
||||
"MaxChannelAllocationPct": 80.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,14 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
|
||||
"Auth": {
|
||||
"AllowDevBypass": false,
|
||||
|
||||
"Microsoft": {
|
||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
||||
},
|
||||
|
||||
"Google": {
|
||||
"ClientId": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -47,8 +47,23 @@
|
||||
* 3. Grant admin consent
|
||||
* 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": {
|
||||
"TenantId": "0be4c23a-6941-4bdb-b397-a4faf88de4b3",
|
||||
"TenantId": "f56a3c51-9b5c-4356-920f-b4dcf932a96b",
|
||||
"ClientId": "b0f29246-91e7-4615-96db-5de9b6f8da2e",
|
||||
"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? BusinessCategory { get; set; }
|
||||
public string? BusinessDescription { get; set; }
|
||||
public string? FirstName { get; set; }
|
||||
public string? LastName { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? ContactEmail { get; set; }
|
||||
public string? ContactPhone { get; set; }
|
||||
@@ -42,7 +44,9 @@ public sealed class RegisterRequest
|
||||
public string? WebsiteUrl { get; set; }
|
||||
public string? BusinessCategory { 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? ContactPhone { 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.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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// Never changes regardless of which block is active below.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
using Microsoft.Identity.Web;
|
||||
using Registration.Data;
|
||||
|
||||
void ConfigureServices(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
// JWT authentication — Entra External ID (CIAM)
|
||||
// Validates Bearer tokens issued by usimclients.ciamlogin.com.
|
||||
// AzureAd config comes from local.settings.json (Functions) or
|
||||
// appsettings.json / environment variables (AspNetCore).
|
||||
// Validates Bearer tokens issued by PositiveSpendClients.ciamlogin.com.
|
||||
// Config keys: AzureAd:Instance / TenantId / ClientId / Audience
|
||||
// In ASP.NET Core mode: appsettings.json + env vars (AzureAd__*)
|
||||
// In Functions mode: local.settings.json AzureAd section (dev only)
|
||||
services.AddAuthentication()
|
||||
.AddMicrosoftIdentityWebApi(config.GetSection("AzureAd"));
|
||||
|
||||
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.
|
||||
// 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.
|
||||
services.AddSingleton<SqlService>();
|
||||
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
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
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.
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// var builder = WebApplication.CreateBuilder(args);
|
||||
//
|
||||
// ConfigureServices(builder.Services, builder.Configuration);
|
||||
// builder.Services.AddControllers();
|
||||
//
|
||||
// // CORS — replace with actual client origin in production
|
||||
// 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();
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// using Microsoft.Azure.Functions.Worker;
|
||||
//
|
||||
// var host = new HostBuilder()
|
||||
// .ConfigureFunctionsWebApplication()
|
||||
// .ConfigureServices((ctx, services) =>
|
||||
// {
|
||||
// services.AddApplicationInsightsTelemetryWorkerService();
|
||||
// services.ConfigureFunctionsApplicationInsights();
|
||||
// ConfigureServices(services, ctx.Configuration);
|
||||
// })
|
||||
// .Build();
|
||||
//
|
||||
// host.Run();
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
{
|
||||
"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')]",
|
||||
"apiVersion": "2019-10-01",
|
||||
"dependsOn": [
|
||||
@@ -50,7 +50,7 @@
|
||||
"resources": [
|
||||
{
|
||||
"kind": "web",
|
||||
"name": "usim-adp-registration",
|
||||
"name": "ps-adp-registration",
|
||||
"type": "microsoft.insights/components",
|
||||
"location": "[parameters('resourceLocation')]",
|
||||
"properties": {},
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"resourceName": {
|
||||
"type": "string",
|
||||
"defaultValue": "usim-adp-registration",
|
||||
"defaultValue": "ps-adp-registration",
|
||||
"metadata": {
|
||||
"description": "Name of the main resource to be created by this template."
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"connectionId": "AzureWebJobsStorage"
|
||||
},
|
||||
"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",
|
||||
"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
|
||||
|
||||
```
|
||||
Prospect → Registration Function → dbRegistration (future)
|
||||
Admin Panel → Management API → Registration Function (proxy)
|
||||
→ spClientManagement (approve → dbAdPlatform)
|
||||
Client SPA (client.positivespend.com)
|
||||
└─► Gateway (portal.positivespend.com)
|
||||
└─► 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 Function never touches `dbAdPlatform`. Management never touches `dbRegistration`.
|
||||
The Registration service 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
|
||||
|
||||
| Method | Route | Auth | Description |
|
||||
|--------|-------|------|-------------|
|
||||
| GET | `/api/registration/pending` | Function Key | List pending applicants |
|
||||
| GET | `/api/registration/{id}` | Function Key | Get single applicant |
|
||||
| POST | `/api/registration/register` | Function Key | New prospect signup |
|
||||
| POST | `/api/registration/{id}/reject` | Function Key | Reject applicant |
|
||||
| POST | `/api/registration/{id}/complete` | Function Key | Mark approved (called after platform client created) |
|
||||
| GET | `/api/registration/health` | Anonymous | Health check |
|
||||
| `POST` | `/api/registration/register` | Bearer (CIAM JWT) | New prospect signup |
|
||||
| `GET` | `/api/registration/pending` | x-functions-key | List pending applicants |
|
||||
| `GET` | `/api/registration/item/{id}` | x-functions-key | Get single applicant |
|
||||
| `POST` | `/api/registration/action/{id}/reject` | x-functions-key | Reject applicant |
|
||||
| `POST` | `/api/registration/action/{id}/complete` | x-functions-key | Mark approved |
|
||||
| `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
|
||||
lifecycle and resets on cold start. No database required.
|
||||
## Authentication
|
||||
|
||||
**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
|
||||
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
|
||||
2. Set `ConnectionStrings:Sql` to the registration database connection string
|
||||
3. In `Program.cs`, swap DI registration:
|
||||
```csharp
|
||||
services.AddSingleton<SqlService>();
|
||||
services.AddSingleton<IRegistrationDataService, SqlDataService>();
|
||||
```
|
||||
Run `dbo.spRegistration.sql` against `dbRegistration` on `10.10.99.212` once.
|
||||
It is idempotent (`CREATE OR ALTER PROCEDURE`, `IF OBJECT_ID ... IS NULL` guard on the table).
|
||||
|
||||
The `SqlDataService` calls `dbo.spRegistration` with the standard `@action/@rqst/@resp OUTPUT`
|
||||
pattern used across all AdPlatform services.
|
||||
|
||||
## Local Development
|
||||
## docker-compose Environment Variables
|
||||
|
||||
```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
|
||||
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,50 +1,54 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Registration</RootNamespace>
|
||||
<!-- SWAP: Azure Functions only — remove for AspNetCore -->
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<!-- SWAP: AzureFunctionsVersion removed for ASP.NET Core mode. -->
|
||||
<!-- To restore Azure Functions: add back <AzureFunctionsVersion>v4</AzureFunctionsVersion> -->
|
||||
<!-- and change Sdk above to Microsoft.NET.Sdk -->
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
SWAP: Azure Functions packages (active)
|
||||
To switch to AspNetCore: comment this ItemGroup, uncomment the one below,
|
||||
and remove <AzureFunctionsVersion> from PropertyGroup above.
|
||||
SWAP: ASP.NET Core packages ◄ ACTIVE
|
||||
Microsoft.NET.Sdk.Web pulls in Microsoft.AspNetCore.App automatically —
|
||||
no explicit FrameworkReference needed here.
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
-->
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.51.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
SWAP: ASP.NET Core packages
|
||||
To switch to AspNetCore: uncomment this ItemGroup, comment the one above,
|
||||
and remove <AzureFunctionsVersion> from PropertyGroup above.
|
||||
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>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.51.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<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 when running as a plain web app.
|
||||
host.json and local.settings.json are inert in ASP.NET Core mode.
|
||||
Kept in the repo so the Azure Functions SWAP path remains intact.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<None Update="host.json">
|
||||
@@ -56,4 +60,4 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
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",
|
||||
"cors": {
|
||||
"allowedOrigins": [
|
||||
"https://adpregist.usimdev.com",
|
||||
"https://register.positivespend.com",
|
||||
"https://client.positivespend.com",
|
||||
"http://localhost:3001"
|
||||
],
|
||||
"allowedHeaders": [ "*" ],
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
|
||||
},
|
||||
"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": {
|
||||
"Instance": "https://usimclients.ciamlogin.com/",
|
||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||
"ClientId": "154c9111-14a0-4c0f-8132-7bc68254a74e",
|
||||
"Audience": "154c9111-14a0-4c0f-8132-7bc68254a74e"
|
||||
"Instance": "https://REPLACE_WITH_CIAM_SUBDOMAIN.ciamlogin.com/",
|
||||
"TenantId": "REPLACE_WITH_CIAM_TENANT_ID",
|
||||
"ClientId": "REPLACE_WITH_CIAM_CLIENT_ID",
|
||||
"Audience": "REPLACE_WITH_CIAM_CLIENT_ID"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user