diff --git a/CloudZen.csproj b/CloudZen.csproj index a710a5d..1a38685 100644 --- a/CloudZen.csproj +++ b/CloudZen.csproj @@ -21,6 +21,7 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index 45e0454..6973a5b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,6 +22,7 @@ + diff --git a/Program.cs b/Program.cs index b8f6f24..39ab45c 100644 --- a/Program.cs +++ b/Program.cs @@ -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 (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 @@ -101,23 +102,27 @@ builder.Services.AddScoped(); // Register AppointmentService for n8n webhook appointment booking -builder.Services.AddScoped(); +builder.Services.AddHttpClient(client => + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); // Register TicketService as the implementation for ITicketService builder.Services.AddScoped(); // Register ResumeService (uses IOptions for configuration) -builder.Services.AddScoped(); +builder.Services.AddHttpClient(client => + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); // Register ApiEmailService as the implementation for IEmailService // Uses IOptions for configuration // This sends emails through the Azure Functions API backend (secure for WebAssembly) -builder.Services.AddScoped(); +builder.Services.AddHttpClient(client => + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); // Register ChatbotService as the implementation for IChatbotService // Uses IOptions for configuration // This sends chat messages through the Azure Functions API backend (API key stays server-side) -builder.Services.AddScoped(); +builder.Services.AddHttpClient(client => + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); // Register ProjectService for managing portfolio projects builder.Services.AddScoped(); diff --git a/docs/02-deployment/AZURE_FUNCTION_DEPLOYMENT.md b/docs/02-deployment/AZURE_FUNCTION_DEPLOYMENT.md index a605438..f4b86b8 100644 --- a/docs/02-deployment/AZURE_FUNCTION_DEPLOYMENT.md +++ b/docs/02-deployment/AZURE_FUNCTION_DEPLOYMENT.md @@ -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. | | `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 | @@ -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.* diff --git a/docs/02-deployment/DEPLOYMENT_CHECKLIST.md b/docs/02-deployment/DEPLOYMENT_CHECKLIST.md index ac7e2a6..82fca04 100644 --- a/docs/02-deployment/DEPLOYMENT_CHECKLIST.md +++ b/docs/02-deployment/DEPLOYMENT_CHECKLIST.md @@ -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 @@ -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 +- [ ] 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 diff --git a/docs/02-deployment/DEPLOYMENT_GUIDE.md b/docs/02-deployment/DEPLOYMENT_GUIDE.md index 6b5da18..8bac82d 100644 --- a/docs/02-deployment/DEPLOYMENT_GUIDE.md +++ b/docs/02-deployment/DEPLOYMENT_GUIDE.md @@ -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.