Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CloudZen.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
<PackageVersion Include="Microsoft.Extensions.Azure" Version="1.11.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="MimeKit" Version="4.15.1" />
<PackageVersion Include="Polly" Version="8.6.5" />
Expand Down
23 changes: 14 additions & 9 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@
// HTTP CLIENT REGISTRATION
// =============================================================================

// Register HttpClient with base address for API calls
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
// NOTE: Each service below gets its OWN HttpClient via AddHttpClient<T> (IHttpClientFactory).
// Do NOT register a single shared HttpClient instance here - ApiEmailService, ChatbotService,
// and AppointmentService each set httpClient.Timeout in their constructor, and HttpClient throws
// InvalidOperationException ("net_http_operation_started") if Timeout is set after any request
// has been sent on that instance. A shared instance crashes as soon as a second service is
// constructed after the first has made a request.

// =============================================================================
// SECURITY NOTES FOR BLAZOR WEBASSEMBLY
Expand All @@ -101,23 +102,27 @@
builder.Services.AddScoped<IBookingService, BookingService>();

// Register AppointmentService for n8n webhook appointment booking
builder.Services.AddScoped<IAppointmentService, AppointmentService>();
builder.Services.AddHttpClient<IAppointmentService, AppointmentService>(client =>
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
Comment on lines +105 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 \
  'class (AppointmentService|ResumeService|ApiEmailService|ChatbotService)|ApiBaseUrl|BaseAddress|GetAsync|PostAsync|SendAsync' \
  --glob '*.cs'

Repository: MyTeamForDev/CloudZen

Length of output: 159


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== tracked .cs Program.cs files =="
git ls-files '*.cs' | rg '/Program\.cs$|Program\.cs$' || true

echo
echo "== all .cs files with service/client/base/url terms =="
rg -n -i -C 4 \
  'AddHttpClient|BaseAddress|ApiBaseUrl|appointment|resume|email|chatbot|GetAsync|PostAsync|SendAsync|HttpClient' \
  --glob '*.cs' . || true

Repository: MyTeamForDev/CloudZen

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Program.cs relevant section =="
sed -n '80,135p' Program.cs | cat -n

echo
echo "== service implementation files =="
for f in \
  Program.cs \
  Features/Contact/Services/ApiEmailService.cs \
  Features/Chat/Services/ChatbotService.cs \
  Features/Profile/Services/ResumeService.cs
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    git ls-files | rg "(\b|\bAppointments/Services/AppointmentService.cs)\b" | head -20
  fi
done

echo
echo "== Http methods and BaseAddress/ApiBaseUrl usage in tracked .cs files =="
rg -n \
  'AddHttpClient<I(AppointmentService|ResumeService|ApiEmailService|ChatbotService)|IProjectService)|HttpClient|BaseAddress|CreateFromApiBase|Get.*AsJsonAsync|Send.*Async|Post.*AsJsonAsync|ChatUrl|SendEmailUrl|ResumeUrl|ApiBaseUrl' \
  --glob '*.cs' \
  | head -200

Repository: MyTeamForDev/CloudZen

Length of output: 7166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re, pathlib

root=pathlib.Path('.')
terms='|'.join([
    r'builder\.Services\.AddHttpClient<',
    r'\.BaseAddress\s*=',
    r'ApiBaseUrl',
    r'ChatUrl|SendEmailUrl',
    r'Get.*AsJsonAsync|Post.*AsJsonAsync|Send.*Async',
    r'GetByteArrayAsync',
])
regex=re.compile(terms)

for path in root.rglob('*.cs'):
    text=path.read_text(errors='ignore')
    if not regex.search(text):
        continue
    print(f'\n## {path}')
    for i,line in enumerate(text.splitlines(),1):
        if regex.search(line):
            print(f'{i:4}: {line}')

services=[
    'Features/Contact/Services/ApiEmailService.cs',
    'Features/Chat/Services/ChatbotService.cs',
    'Features/Profile/Services/ResumeService.cs',
]
for svc in services:
    p=pathlib.Path(svc)
    print(f'\n## {p} relevant methods')
    lines=p.read_text(errors='ignore').splitlines()
    for start,end in [(1,80),(80,170),(170,230)]:
        text='\n'.join(lines[s-1:e])
        if any(s in text for s in ['PostAsJsonAsync','GetByteArrayAsync','DeleteAsync','string url']):
            print(f'--- lines {start}-{end} ---')
            print(text)
PY

echo
echo "== find AppointmentService files =="
git ls-files | rg 'Appointment(Service|s).*\.cs$|Appointments/Services' || true

Repository: MyTeamForDev/CloudZen

Length of output: 4282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for svc in [
    "Features/Booking/Services/AppointmentService.cs",
    "Features/Contact/Services/ApiEmailService.cs",
    "Features/Chat/Services/ChatbotService.cs",
    "Features/Profile/Services/ResumeService.cs",
]:
    p=Path(svc)
    if not p.exists():
        print(f"MISSING {svc}")
        continue
    text=p.read_text(errors="ignore")
    print(f"\n## {p}")
    start=len(text)
    for meth in ["CreateFromApiBase", "SendAsync", "PostAsJsonAsync", "GetByteArrayAsync"]:
        idx=text.find(meth)
        if idx>=0 and idx<start:
            start=idx
    print(text[start:start+1200])
PY

echo
echo "== deterministic Uri/BaseAddress behavior =="
python3 - <<'PY'
from urllib.parse import urljoin
host='https://app.example/'
local='http://localhost:7257/api'
for path in ['/api', 'api/', '/api/send-email']:
    print(path, 'with host BaseAddress ->', urljoin(host, path))
    print(path, 'with local BaseAddress ->', urljoin(local, path))
PY

echo
echo "== check SendAsJsonAsync extension signature/source availability =="
rg -n "CreateFromApiBase|IAppointmentService|AddHttpClient<IAppointmentService|localUrl|functionsLocalUrl|ApiBaseUrl" Program.cs Features/Booking Features/Contact Features/Chat Features/Profile --glob '*.cs'

Repository: MyTeamForDev/CloudZen

Length of output: 7526


Configure the typed HTTP clients from the local functions URL.

builder.Configuration["{Service}:ApiBaseUrl"] = functionsLocalUrl is set, but the typed clients override BaseAddress to builder.HostEnvironment.BaseAddress. Since AppointmentService sends relative requests and ApiEmailService/ChatbotService compose endpoint paths from ApiBaseUrl, these requests will be routed to the WebAssembly origin in development. Set each typed client’s BaseAddress from its ApiBaseUrl; use the absolute ResumeUrl for ResumeService.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Program.cs` around lines 105 - 106, Update the typed HTTP client
registrations for AppointmentService, ApiEmailService, ChatbotService, and
ResumeService to use their configured ApiBaseUrl values instead of
builder.HostEnvironment.BaseAddress. Resolve the first three base addresses from
each service’s “{Service}:ApiBaseUrl” configuration entry, and configure
ResumeService with the absolute ResumeUrl.


// Register TicketService as the implementation for ITicketService
builder.Services.AddScoped<ITicketService, TicketService>();

// Register ResumeService (uses IOptions<BlobStorageOptions> for configuration)
builder.Services.AddScoped<ResumeService>();
builder.Services.AddHttpClient<ResumeService>(client =>
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

// Register ApiEmailService as the implementation for IEmailService
// Uses IOptions<EmailServiceOptions> for configuration
// This sends emails through the Azure Functions API backend (secure for WebAssembly)
builder.Services.AddScoped<IEmailService, ApiEmailService>();
builder.Services.AddHttpClient<IEmailService, ApiEmailService>(client =>
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

// Register ChatbotService as the implementation for IChatbotService
// Uses IOptions<ChatbotOptions> for configuration
// This sends chat messages through the Azure Functions API backend (API key stays server-side)
builder.Services.AddScoped<IChatbotService, ChatbotService>();
builder.Services.AddHttpClient<IChatbotService, ChatbotService>(client =>
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

// Register ProjectService for managing portfolio projects
builder.Services.AddScoped<IProjectService, ProjectService>();
Expand Down
4 changes: 2 additions & 2 deletions docs/02-deployment/AZURE_FUNCTION_DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ Add these application settings:
| `ANTHROPIC_API_KEY` | `your-anthropic-api-key` | Anthropic Claude API key (for AI chatbot) |
| `BREVO_SMTP_KEY` | `your-brevo-smtp-key` | Brevo SMTP relay key |
| `BREVO_SMTP_LOGIN` | `your-smtp-login@smtp-brevo.com` | Brevo SMTP login |
| `N8N_WEBHOOK_URL` | `https://cloudzen-n8n.pikapod.net/webhook/appointments` | n8n appointment booking webhook — Production URL only, workflow must be activated (required — 502 if unset). Booking DB (Neon Postgres) is configured inside n8n itself, not an Azure resource. |
| `N8N_WEBHOOK_URL` | `https://cloudzen-n8n.pikapod.net/webhook/appointments` | n8n appointment booking webhook — Production URL only, workflow must be activated (required — 502 if unset). Booking DB (Neon Postgres) is configured inside n8n itself, not an Azure resource. See [../08-N8N/appointment-system-v3.md](../08-N8N/appointment-system-v3.md) for workflow setup. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the missing-configuration status code.

BookAppointmentFunction.cs returns HTTP 500 when N8N_WEBHOOK_URL is unset, not 502. Update the description to prevent incorrect incident diagnosis.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/02-deployment/AZURE_FUNCTION_DEPLOYMENT.md` at line 187, Update the
N8N_WEBHOOK_URL documentation entry to state that an unset value causes
BookAppointmentFunction.cs to return HTTP 500 instead of HTTP 502. Preserve the
existing production URL, activation requirement, database configuration, and
workflow setup guidance.

| `KEY_VAULT_ENDPOINT` | `https://cloudzenvault.vault.azure.net/` | *(Optional)* Azure Key Vault URI for secrets management |
| `EmailSettings:FromEmail` | `cloudzen.inc@gmail.com` | Sender email address |
| `EmailSettings:CcEmail` | `softevolutionsl@gmail.com` | CC email address |
Expand Down Expand Up @@ -604,4 +604,4 @@ CloudZen/

---

*Last updated: June 2025 -- Updated Function App name, URLs, CORS configuration (multi-level), Key Vault integration, Polly rate limiting, environment-specific Blazor config, security headers, file structure, and local development setup.*
*Last updated: 2026-07-25 -- Added cross-reference to n8n workflow docs (08-N8N/appointment-system-v3.md) for N8N_WEBHOOK_URL.*
23 changes: 8 additions & 15 deletions docs/02-deployment/DEPLOYMENT_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Azure Deployment Quick Reference

> **See also:** [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) · [AZURE_FUNCTION_DEPLOYMENT.md](AZURE_FUNCTION_DEPLOYMENT.md)
>
> **Last synced: 2026-07-25**

## ⚡ Quick Start Checklist

### 🚨 Before You Begin - CRITICAL
Expand Down Expand Up @@ -29,21 +33,10 @@
- [ ] Configure CORS (allow your Static Web App domain)

### 3️⃣ Azure Functions Backend (REQUIRED for secure operations)
```bash
# Create the backend project:
dotnet new func -n CloudZen.Api
cd CloudZen.Api
dotnet add package Azure.Identity
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
dotnet add package MailKit
dotnet add package Polly
```
- [ ] Create `SendEmailFunction.cs` (see `DEPLOYMENT_GUIDE.md` section 5)
- [ ] Create `ChatFunction.cs` (AI chatbot proxy to Anthropic Claude)
- [ ] Deploy to Azure Function App (Consumption plan)
- [ ] Enable Managed Identity
- [ ] Link to Static Web App (in Azure Portal: Static Web App > APIs)
- [ ] Set `ANTHROPIC_API_KEY` in Azure Function App settings
- [x] Backend created and deployed (`CloudZen.Api`, Consumption plan) — see [AZURE_FUNCTION_DEPLOYMENT.md](AZURE_FUNCTION_DEPLOYMENT.md) for setup
- [x] `SendEmailFunction.cs` and `ChatFunction.cs` deployed
- [x] Managed Identity enabled, linked to Static Web App

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the Managed Identity relationship.

The deployment guide documents a system-assigned identity on the Function App with Key Vault access; it does not establish a Managed Identity link to the Static Web App. Reword this item to reflect the actual Function App/Key Vault configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/02-deployment/DEPLOYMENT_CHECKLIST.md` at line 38, Update the deployment
checklist item to describe the system-assigned Managed Identity enabled on the
Function App and its Key Vault access, removing the inaccurate reference to
linking it with the Static Web App.

- [ ] Verify all env vars set in Function App settings — see [AZURE_FUNCTION_DEPLOYMENT.md](AZURE_FUNCTION_DEPLOYMENT.md#azure-portal-configuration)

### 4️⃣ Azure Key Vault Setup
```bash
Expand Down
26 changes: 5 additions & 21 deletions docs/02-deployment/DEPLOYMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,11 @@ Azure Static Web Apps requires a configuration file for routing, security header
}
```

### Configure Static Web App Application Settings:

1. Go to your Static Web App in the Azure Portal.
2. Select **Configuration** under **Settings**.
3. Add these application settings (these will be available as environment variables):

| Name | Value | Purpose |
|------|-------|---------|
| `BREVO_SMTP_KEY` | Your Brevo SMTP relay key | Email service (for Azure Function backend) |
| `BREVO_SMTP_LOGIN` | Your Brevo SMTP login | Email service (for Azure Function backend) |
| `BLOB_STORAGE_CONNECTION_STRING` | Your storage connection string | Blob operations (for Azure Function backend) |
| `ANTHROPIC_API_KEY` | Your Anthropic Claude API key | AI chatbot (ChatFunction) |
| `N8N_WEBHOOK_URL` | Your n8n production webhook URL | Appointment booking webhook (required — 502 if unset) |
| `KEY_VAULT_ENDPOINT` | Your Key Vault URI | *(Optional)* Loads secrets from Key Vault via Managed Identity |
| `ProductionOrigin` | Your Static Web App URL | CORS allowed origin |
| `AllowedOrigins:0` / `AllowedOrigins:1` | Explicit origin URLs | *(Optional)* Overrides default CORS origin list |
| `RateLimiting:PermitLimit` | e.g. `10` | Max requests per window |
| `RateLimiting:WindowSeconds` | e.g. `60` | Rate limit window in seconds |
| `RateLimiting:QueueLimit` | e.g. `0` | Queue limit for excess requests |
| `RateLimiting:InactivityTimeoutMinutes` | e.g. `5` | Timeout for inactive limiters |
| `RateLimiting:EnableCircuitBreaker` | `true`/`false` | Enable Polly circuit breaker pattern |
### Function App Environment Variables

Runtime config/secrets (`BREVO_SMTP_KEY`, `N8N_WEBHOOK_URL`, `ANTHROPIC_API_KEY`, rate limiting, CORS origins, etc.) are set on the **Azure Function App** — not the Static Web App. These are two separate Azure resources with separate Configuration blades.

**Authoritative table:** see [AZURE_FUNCTION_DEPLOYMENT.md](AZURE_FUNCTION_DEPLOYMENT.md#azure-portal-configuration) for the current, complete list (kept in one place to avoid drift).

**Note:** These environment variables are **only accessible to Azure Functions**, not to your Blazor WebAssembly app directly.

Expand Down
Loading