Compare commits
20 Commits
65672c8716
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce747ce48 | ||
|
|
b4fd0b6c9e | ||
|
|
0e17da63d0 | ||
|
|
a6df344e80 | ||
|
|
fae2226581 | ||
|
|
8de463cd17 | ||
|
|
866ab983c5 | ||
|
|
44764bc641 | ||
| 37b08ef012 | |||
|
|
04e2bb40e8 | ||
|
|
f0c3b0f917 | ||
|
|
755274dee6 | ||
|
|
86852658b8 | ||
|
|
1ea8716ac6 | ||
|
|
9fa2c774a7 | ||
|
|
967f04ebbc | ||
|
|
6e888cf7a8 | ||
|
|
f799774adc | ||
|
|
8dd0e11b99 | ||
|
|
07a7489bfc |
43
.gitea/workflows/creative.yml
Normal file
43
.gitea/workflows/creative.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Creative
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'Creative/**'
|
||||
- 'Shared/**'
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/creative.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/creative
|
||||
SERVICE: creative
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build Creative/Creative.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f Creative/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
43
.gitea/workflows/gateway.yml
Normal file
43
.gitea/workflows/gateway.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Gateway
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'Gateway/**'
|
||||
- 'Shared/**' # adjust to your actual shared project folder name
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/gateway.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/gateway
|
||||
SERVICE: gateway
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build Gateway/Gateway.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f Gateway/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
43
.gitea/workflows/googleapi.yml
Normal file
43
.gitea/workflows/googleapi.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: GoogleApi
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'GoogleApi/**'
|
||||
- 'Shared/**'
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/googleapi.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/googleapi
|
||||
SERVICE: googleapi
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build GoogleApi/GoogleApi.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f GoogleApi/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
43
.gitea/workflows/intelligenceapi.yml
Normal file
43
.gitea/workflows/intelligenceapi.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: IntelligenceApi
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'IntelligenceApi/**'
|
||||
- 'Shared/**'
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/intelligenceapi.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/intelligenceapi
|
||||
SERVICE: intelligenceapi
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build IntelligenceApi/IntelligenceApi.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f IntelligenceApi/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
43
.gitea/workflows/management.yml
Normal file
43
.gitea/workflows/management.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Management
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'Management/**'
|
||||
- 'Shared/**'
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/management.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/management
|
||||
SERVICE: management
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build Management/Management.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f Management/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
43
.gitea/workflows/metaapi.yml
Normal file
43
.gitea/workflows/metaapi.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: MetaApi
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'MetaApi/**'
|
||||
- 'Shared/**'
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/metaapi.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/metaapi
|
||||
SERVICE: metaapi
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build MetaApi/MetaApi.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f MetaApi/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
43
.gitea/workflows/registration.yml
Normal file
43
.gitea/workflows/registration.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Registration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'Registration/**'
|
||||
- 'Shared/**'
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/registration.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/registration
|
||||
SERVICE: registration
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build Registration/Registration.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f Registration/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
43
.gitea/workflows/tiktokapi.yml
Normal file
43
.gitea/workflows/tiktokapi.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: TikTokApi
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'TikTokApi/**'
|
||||
- 'Shared/**'
|
||||
- 'AdPlatformServers.sln'
|
||||
- '.gitea/workflows/tiktokapi.yml'
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: host
|
||||
env:
|
||||
REGISTRY: 10.10.25.211:5000
|
||||
IMAGE: adplatform/tiktokapi
|
||||
SERVICE: tiktokapi
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build service
|
||||
run: dotnet build TikTokApi/TikTokApi.csproj --configuration Release
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
podman build \
|
||||
--layers \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||
-f TikTokApi/Dockerfile \
|
||||
.
|
||||
|
||||
- name: Push to local registry
|
||||
run: podman push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||
|
||||
- name: Deploy on app server
|
||||
run: |
|
||||
ssh -i ~/.ssh/gitea_runner root@10.10.25.211 \
|
||||
"podman pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest && \
|
||||
cd /opt/adplatform && \
|
||||
podman-compose up -d --no-deps --force-recreate ${{ env.SERVICE }}"
|
||||
@@ -17,7 +17,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MetaApi", "MetaApi\MetaApi.
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TikTokApi", "TikTokApi\TikTokApi.csproj", "{90100339-E52D-4E6B-9F14-B034192508E8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntelligenceAPI", "IntelligenceAPI\IntelligenceAPI.csproj", "{1971AA11-806A-4482-BFA5-8C9479E6EDF3}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntelligenceApi", "IntelligenceApi\IntelligenceApi.csproj", "{1971AA11-806A-4482-BFA5-8C9479E6EDF3}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
|
||||
16
Creative/Dockerfile
Normal file
16
Creative/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Creative/Creative.csproj", "Creative/"]
|
||||
RUN dotnet restore "Creative/Creative.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Creative"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Creative.dll"]
|
||||
16
Gateway/Dockerfile
Normal file
16
Gateway/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Gateway/Gateway.csproj", "Gateway/"]
|
||||
RUN dotnet restore "Gateway/Gateway.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Gateway"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Gateway.dll"]
|
||||
@@ -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();
|
||||
|
||||
@@ -239,13 +239,13 @@ public sealed class ClientAuthMiddleware
|
||||
var data = root.TryGetProperty("data", out var dataProp) ? dataProp : root;
|
||||
|
||||
clientContext.SessionId = data.TryGetProperty("sessionId", out var sid) ? sid.GetString() : null;
|
||||
clientContext.ClientId = data.TryGetProperty("clientId", out var cid) ? cid.GetString() : null;
|
||||
clientContext.ClientName = data.TryGetProperty("clientName", out var cn) ? cn.GetString() : null;
|
||||
clientContext.ClientId = data.TryGetProperty("clientId", out var cid) ? cid.GetString() : null;
|
||||
clientContext.ClientName = data.TryGetProperty("clientName", out var cn) ? cn.GetString() : null;
|
||||
clientContext.ClientCategory = data.TryGetProperty("clientCategory", out var ccat) ? ccat.GetString() : null;
|
||||
clientContext.UserId = data.TryGetProperty("userId", out var uid) ? uid.GetString() : null;
|
||||
clientContext.Email = data.TryGetProperty("userEmail", out var ue) ? ue.GetString() : null;
|
||||
clientContext.Role = data.TryGetProperty("role", out var role) ? role.GetString() : null;
|
||||
clientContext.IsDevBypass = false;
|
||||
clientContext.UserId = data.TryGetProperty("userId", out var uid) ? uid.GetString() : null;
|
||||
clientContext.Email = data.TryGetProperty("userEmail", out var ue) ? ue.GetString() : null;
|
||||
clientContext.Role = data.TryGetProperty("role", out var role) ? role.GetString() : null;
|
||||
clientContext.IsDevBypass = false;
|
||||
|
||||
_logger.LogWarning("[Auth] Session validated OK | ClientId={ClientId} Email={Email} | Corr={Corr}",
|
||||
clientContext.ClientId, clientContext.Email, corrId);
|
||||
@@ -354,10 +354,15 @@ public sealed class ClientAuthMiddleware
|
||||
if (!string.IsNullOrWhiteSpace(audienceOverride))
|
||||
validAudiences.Add(audienceOverride);
|
||||
|
||||
// fix applied
|
||||
var validationParams = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuers = new[] { $"{instance.TrimEnd('/')}/{tenantId}/v2.0" },
|
||||
ValidIssuers = new[]
|
||||
{
|
||||
$"https://login.microsoftonline.com/{tenantId}/v2.0",
|
||||
$"https://sts.windows.net/{tenantId}/"
|
||||
},
|
||||
|
||||
ValidateAudience = true,
|
||||
ValidAudiences = validAudiences,
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
GoogleApi/Dockerfile
Normal file
16
GoogleApi/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["GoogleApi/GoogleApi.csproj", "GoogleApi/"]
|
||||
RUN dotnet restore "GoogleApi/GoogleApi.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/GoogleApi"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "GoogleApi.dll"]
|
||||
16
IntelligenceApi/Dockerfile
Normal file
16
IntelligenceApi/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["IntelligenceApi/IntelligenceApi.csproj", "IntelligenceApi/"]
|
||||
RUN dotnet restore "IntelligenceApi/IntelligenceApi.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/IntelligenceApi"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "IntelligenceApi.dll"]
|
||||
16
Management/Dockerfile
Normal file
16
Management/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Management/Management.csproj", "Management/"]
|
||||
RUN dotnet restore "Management/Management.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Management"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Management.dll"]
|
||||
@@ -8,6 +8,25 @@ 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
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
@@ -62,6 +81,9 @@ app.MapGet("/", () => Results.Ok(new
|
||||
}
|
||||
}));
|
||||
|
||||
// CORS — must be before auth middleware
|
||||
app.UseCors();
|
||||
|
||||
// Authentication middleware
|
||||
app.UseMiddleware<ClientAuthMiddleware>();
|
||||
|
||||
@@ -71,4 +93,4 @@ app.UseMiddleware<ActivityLoggingMiddleware>();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
@@ -255,7 +255,7 @@ public sealed class ClientAuthMiddleware
|
||||
|
||||
var tenantId = _config["Auth:Staff:TenantId"];
|
||||
var clientId = _config["Auth:Staff:ClientId"];
|
||||
var instance = _config["Auth:Staff:Instance"] ?? "https://usimclients.ciamlogin.com/";
|
||||
var instance = _config["Auth:Staff:Instance"] ?? "https://login.microsoftonline.com/";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(clientId))
|
||||
return false;
|
||||
@@ -277,7 +277,11 @@ public sealed class ClientAuthMiddleware
|
||||
var validationParams = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuers = new[] { $"{instance.TrimEnd('/')}/{tenantId}/v2.0" },
|
||||
ValidIssuers = new[]
|
||||
{
|
||||
$"https://login.microsoftonline.com/{tenantId}/v2.0",
|
||||
$"https://sts.windows.net/{tenantId}/"
|
||||
},
|
||||
ValidateAudience = true,
|
||||
ValidAudiences = new[] { clientId, $"api://{clientId}" },
|
||||
ValidateLifetime = true,
|
||||
|
||||
@@ -10,22 +10,26 @@
|
||||
"AllowDevBypass": false,
|
||||
|
||||
/*
|
||||
* STAFF IDENTITY — Entra External ID (dev) / Entra org tenant (prod)
|
||||
* STAFF IDENTITY - Microsoft Entra ID (positivespend tenant)
|
||||
*
|
||||
* PRODUCTION MIGRATION: update these three environment variables only.
|
||||
* No code changes required.
|
||||
* App registration: AdPlatform Management Staff API (af95fa13) in positivespend tenant f56a3c51.
|
||||
* The Tech SPA (846a3677) authenticates against this same tenant and
|
||||
* requests scope api://af95fa13-.../access_as_user.
|
||||
*
|
||||
* Auth__Staff__Instance → https://login.microsoftonline.com/
|
||||
* Auth__Staff__TenantId → new company org tenant ID
|
||||
* Auth__Staff__ClientId → staff app registration in org tenant
|
||||
* Management validates JWTs:
|
||||
* issuer = login.microsoftonline.com/f56a3c51/v2.0
|
||||
* audience = af95fa13 or api://af95fa13
|
||||
* roles = Staff.Admin | Staff.Tech
|
||||
*
|
||||
* DEV: CIAM tenant used as placeholder (staff/client login looks identical).
|
||||
* The API-level audience isolation is real regardless of tenant.
|
||||
* These are the correct defaults - also set as env vars on the container:
|
||||
* Auth__Staff__Instance = https://login.microsoftonline.com/
|
||||
* Auth__Staff__TenantId = f56a3c51-9b5c-4356-920f-b4dcf932a96b
|
||||
* Auth__Staff__ClientId = af95fa13-2ef4-4911-b137-7acc6a784cfa
|
||||
*/
|
||||
"Staff": {
|
||||
"Instance": "https://usimclients.ciamlogin.com/",
|
||||
"TenantId": "891f98f1-ed34-42a1-9b6c-28b0554d92c2",
|
||||
"ClientId": "STAFF_APP_CLIENT_ID"
|
||||
"Instance": "https://login.microsoftonline.com/",
|
||||
"TenantId": "f56a3c51-9b5c-4356-920f-b4dcf932a96b",
|
||||
"ClientId": "af95fa13-2ef4-4911-b137-7acc6a784cfa"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -43,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": ""
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ public sealed class InternalController : ControllerBase
|
||||
public IActionResult Health()
|
||||
{
|
||||
_logger.LogDebug("[InternalController] Health check");
|
||||
_logger.LogDebug("[InternalController] Useless Double Check To test change");
|
||||
return Ok(new
|
||||
{
|
||||
ok = true,
|
||||
|
||||
16
MetaApi/Dockerfile
Normal file
16
MetaApi/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["MetaApi/MetaApi.csproj", "MetaApi/"]
|
||||
RUN dotnet restore "MetaApi/MetaApi.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/MetaApi"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "MetaApi.dll"]
|
||||
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; }
|
||||
|
||||
16
Registration/Dockerfile
Normal file
16
Registration/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Registration/Registration.csproj", "Registration/"]
|
||||
RUN dotnet restore "Registration/Registration.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Registration"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Registration.dll"]
|
||||
@@ -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;
|
||||
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,13 +26,36 @@ namespace Registration.Functions;
|
||||
/// entraSubjectId is extracted from the validated
|
||||
/// token — the client never supplies it.
|
||||
///
|
||||
/// GET /api/registration/pending — Admin; requires Function key.
|
||||
/// GET /api/registration/item/{id} — Admin; requires Function key.
|
||||
/// POST /api/registration/action/{id}/reject — Admin; requires Function key.
|
||||
/// POST /api/registration/action/{id}/complete — Admin; requires Function key.
|
||||
/// GET /api/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.
|
||||
///
|
||||
/// ═══════════════════════════════════════════════════════
|
||||
/// HOST SWAP — three files change, everything else stays:
|
||||
/// 1. Functions/RegistrationFunctions.cs ← this file
|
||||
/// 2. Program.cs
|
||||
/// 3. Registration.csproj
|
||||
///
|
||||
/// 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>
|
||||
public class RegistrationFunctions
|
||||
|
||||
// ── 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;
|
||||
private readonly ILogger<RegistrationFunctions> _log;
|
||||
@@ -38,7 +70,7 @@ public class RegistrationFunctions
|
||||
public RegistrationFunctions(IRegistrationDataService data, ILogger<RegistrationFunctions> log)
|
||||
{
|
||||
_data = data;
|
||||
_log = log;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
@@ -46,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
|
||||
@@ -54,21 +87,22 @@ public class RegistrationFunctions
|
||||
|
||||
// ── Public: Register ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Register a new prospect.
|
||||
///
|
||||
/// AuthorizationLevel.Anonymous at the trigger allows any caller with a valid
|
||||
/// Bearer token — no Function key required from the browser.
|
||||
/// The [Authorize] attribute ensures the JWT is present and valid before
|
||||
/// the function body runs. entraSubjectId is extracted from token claims only.
|
||||
/// </summary>
|
||||
[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)
|
||||
public async Task<IActionResult> Register(CancellationToken ct)
|
||||
// ── SWAP: Azure Functions ◄ INACTIVE — uncomment to restore
|
||||
// [Function("Register")]
|
||||
// [Authorize]
|
||||
// public async Task<IActionResult> Register(
|
||||
// [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/register")] HttpRequest req,
|
||||
// CancellationToken ct)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// Identity comes from the validated JWT — never from the request body
|
||||
// ── 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);
|
||||
|
||||
if (string.IsNullOrEmpty(entraSubjectId))
|
||||
@@ -90,7 +124,7 @@ public class RegistrationFunctions
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.BusinessName))
|
||||
return new BadRequestObjectResult(new { ok = false, error = "businessName is required" });
|
||||
|
||||
// Stamp the server-validated identity — client-supplied value is ignored
|
||||
// Stamp from the validated token — never trust the request body for this.
|
||||
request.EntraSubjectId = entraSubjectId;
|
||||
|
||||
_log.LogInformation("[Registration] POST register: {Name} by entra={EntraId}",
|
||||
@@ -106,11 +140,16 @@ public class RegistrationFunctions
|
||||
|
||||
// ── Admin: List pending ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>List all pending registrations. Called by Management API with Function key.</summary>
|
||||
[Function("GetPending")]
|
||||
public async Task<IActionResult> GetPending(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/pending")] HttpRequest req,
|
||||
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");
|
||||
var result = await _data.GetPendingAsync(ct);
|
||||
@@ -119,12 +158,17 @@ public class RegistrationFunctions
|
||||
|
||||
// ── Admin: Get by ID ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Get a single applicant by registration ID. Called by Management API.</summary>
|
||||
[Function("GetById")]
|
||||
public async Task<IActionResult> GetById(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "registration/item/{registrationId}")] HttpRequest req,
|
||||
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);
|
||||
|
||||
@@ -137,13 +181,22 @@ public class RegistrationFunctions
|
||||
|
||||
// ── Admin: Reject ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reject a pending applicant. Called by Management after admin clicks Reject.</summary>
|
||||
[Function("Reject")]
|
||||
public async Task<IActionResult> Reject(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/reject")] HttpRequest req,
|
||||
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)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// ── 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); }
|
||||
catch { /* optional body */ }
|
||||
@@ -159,19 +212,24 @@ public class RegistrationFunctions
|
||||
return new OkObjectResult(result);
|
||||
}
|
||||
|
||||
// ── Admin: Complete (approved) ────────────────────────────────────────
|
||||
// ── Admin: Complete ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Mark a registration as approved/completed.
|
||||
/// Called by Management after spClientManagement.create succeeds.
|
||||
/// Receives the platformClientId to link the registration to the platform record.
|
||||
/// </summary>
|
||||
[Function("Complete")]
|
||||
public async Task<IActionResult> Complete(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "registration/action/{registrationId}/complete")] HttpRequest req,
|
||||
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)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// ── 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); }
|
||||
catch { /* optional body */ }
|
||||
@@ -189,16 +247,21 @@ public class RegistrationFunctions
|
||||
|
||||
// ── Health ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Health check — anonymous, no auth required.</summary>
|
||||
[Function("Health")]
|
||||
public IActionResult Health(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequest req)
|
||||
// ── 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
|
||||
});
|
||||
}
|
||||
@@ -208,7 +271,7 @@ public class RegistrationFunctions
|
||||
|
||||
internal sealed class RejectBody
|
||||
{
|
||||
public string? Reason { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public string? RejectedBy { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,78 @@
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Shared service registrations — identical in both hosting modes.
|
||||
// Never changes regardless of which block is active below.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
using Microsoft.Identity.Web;
|
||||
using Registration.Data;
|
||||
using Registration.Mock;
|
||||
|
||||
var host = new HostBuilder()
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddApplicationInsightsTelemetryWorkerService();
|
||||
services.ConfigureFunctionsApplicationInsights();
|
||||
void ConfigureServices(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
// JWT authentication — Entra External ID (CIAM)
|
||||
// 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"));
|
||||
|
||||
// =============================================================
|
||||
// JWT Authentication — Entra External ID (CIAM)
|
||||
// Validates Bearer tokens issued by usimclients.ciamlogin.com.
|
||||
// AzureAd config is in local.settings.json (dev) or Function App
|
||||
// Configuration (production) using AzureAd__ prefix.
|
||||
// =============================================================
|
||||
services.AddAuthentication()
|
||||
.AddMicrosoftIdentityWebApi(context.Configuration.GetSection("AzureAd"));
|
||||
services.AddAuthorization();
|
||||
|
||||
services.AddAuthorization();
|
||||
// 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 → dbRegistration on 10.10.99.212
|
||||
// Calls dbo.spRegistration with @action/@rqst/@resp OUTPUT pattern.
|
||||
services.AddSingleton<SqlService>();
|
||||
services.AddSingleton<IRegistrationDataService, SqlDataService>();
|
||||
// ── swap data layer here if needed ──────────────────────────────────────
|
||||
// services.AddSingleton<IRegistrationDataService, MockDataService>();
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Data layer — SqlDataService backed by dbRegistration.
|
||||
// Connection string: ConnectionStrings:Sql in local.settings.json
|
||||
// or the "Sql" connection string in Function App Configuration.
|
||||
// =============================================================
|
||||
services.AddSingleton<SqlService>();
|
||||
services.AddSingleton<IRegistrationDataService, SqlDataService>();
|
||||
})
|
||||
.Build();
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// 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);
|
||||
|
||||
host.Run();
|
||||
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
|
||||
// Also update RegistrationFunctions.cs and Registration.csproj.
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// 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,14 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Registration</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Registration</RootNamespace>
|
||||
<!-- 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: ASP.NET Core packages ◄ ACTIVE
|
||||
Microsoft.NET.Sdk.Web pulls in Microsoft.AspNetCore.App automatically —
|
||||
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>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.51.0" />
|
||||
@@ -20,15 +44,20 @@
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="host.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<!--
|
||||
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">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</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"
|
||||
}
|
||||
}
|
||||
|
||||
16
TikTokApi/Dockerfile
Normal file
16
TikTokApi/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["TikTokApi/TikTokApi.csproj", "TikTokApi/"]
|
||||
RUN dotnet restore "TikTokApi/TikTokApi.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/TikTokApi"
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "TikTokApi.dll"]
|
||||
16
ci.yaml
16
ci.yaml
@@ -1,16 +0,0 @@
|
||||
name: CI Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: host
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Say hello
|
||||
run: echo "CI runner is working!"
|
||||
Reference in New Issue
Block a user