Remove Azure deployment stack; Proxmox 7810 is canonical production.
Delete Static Web Apps workflow, Bicep infra, Azure Functions api/, SWA configs, and Azure-only scripts; document Proxmox deploy path and refresh QuickStart and prerequisites. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,146 +0,0 @@
|
||||
---
|
||||
description: Generate and deploy Azure Functions with comprehensive planning, code generation, and deployment automation.
|
||||
tools: ["changes","edit","extensions","fetch","findTestFiles","githubRepo","new","openSimpleBrowser","problems","runCommands","runNotebooks","runTasks","search","testFailure","todos","usages","vscodeAPI","Microsoft Docs","azureterraformbestpractices","bicepschema","deploy","quota","get_bestpractices","azure_query_azure_resource_graph","azure_generate_azure_cli_command","azure_get_auth_state","azure_get_current_tenant","azure_get_available_tenants","azure_set_current_tenant","azure_get_selected_subscriptions","azure_open_subscription_picker","azure_sign_out_azure_user","azure_diagnose_resource","azure_list_activity_logs"]
|
||||
model: Claude Sonnet 4
|
||||
---
|
||||
|
||||
# Azure Functions Code Generation and Deployment
|
||||
|
||||
Enterprise-grade Azure Functions development workflow with automated planning, code generation, testing, and deployment using Azure best practices and Infrastructure as Code (IaC).
|
||||
|
||||
## Core Workflow
|
||||
Make sure to ask the user to confirm to move forward with each step.
|
||||
|
||||
### 1. Planning Phase
|
||||
- **Architecture Definition**: Define function structure, components, and configurations by considering the best practices for both code generation and deployment
|
||||
- **Technology Stack**: Specify programming language, runtime version, and tools
|
||||
- **Resource Requirements**: Identify Azure resources and consumption plans
|
||||
- **Validation Strategy**: Define testing approaches and success criteria
|
||||
- **Documentation**: Save plan to `azure_functions_codegen_and_deployment_plan.md`
|
||||
|
||||
### 2. Status Tracking
|
||||
- **Progress Monitoring**: Track completion of each phase with detailed status
|
||||
- **Error Handling**: Log failures and recovery steps for troubleshooting
|
||||
- **Documentation**: Maintain `azure_functions_codegen_and_deployment_status.md`
|
||||
|
||||
### 3. Code Generation
|
||||
- **Prerequisites**: Verify development tools and runtime versions
|
||||
- **Best Practices**: Apply Azure Functions and general code generation standards. Invoke the `get_bestpractices` tool twice to collect recommendations from both perspectives:
|
||||
- Call with resource = `azurefunctions` and action = `code-generation` to get Azure Functions specific code generation best practices.
|
||||
- Call with resource = `general` and action = `code-generation` to get general Azure code generation best practices.
|
||||
Combine the results and apply relevant recommendations from both responses.
|
||||
- **Security**: Set appropriate authentication levels (default: `function`)
|
||||
- **Structure**: Follow language-specific project layouts and conventions
|
||||
- **Python**: Do not use grpcio dependent packages such as azure-functions-worker, unless necessary
|
||||
- **JavaScript v4 Structure**:
|
||||
```
|
||||
root/
|
||||
├── host.json # Function host configuration
|
||||
├── local.settings.json # Development settings
|
||||
├── package.json # Dependencies
|
||||
├── src/
|
||||
│ ├── app.js # Main application entry
|
||||
│ └── [modules].js # Business logic
|
||||
└── tests/ # Test suite
|
||||
```
|
||||
|
||||
### 4. Local Validation
|
||||
Start the function app locally and carefully monitor the startup output. Look for any errors, warnings, or unusual messages.
|
||||
Don't proceed to testing until you've confirmed a clean startup. If you see any issues, investigate and fix them before continuing.
|
||||
- **Testing**: Achieve 80%+ code coverage with comprehensive test suite
|
||||
- **Execution**: Validate local function execution and performance
|
||||
- **Process Management**: Clean shutdown of existing instances of the function app before restart
|
||||
- macOS/Linux: `pkill -9 -f func`
|
||||
- Windows: `taskkill /F /IM func.exe /T`
|
||||
#### Post-Testing Cleanup Protocol
|
||||
Upon finishing testing, ensure all processes are properly shut down to prevent resource conflicts and port binding issues:
|
||||
|
||||
### 5. Deployment
|
||||
- **Infrastructure**: Refer to the following GitHub repos for best practices on generating Bicep templates using Azure Verified Modules (AVM):
|
||||
- #githubRepo: https://github.com/Azure-Samples/functions-quickstart-javascript-azd/tree/main/infra
|
||||
- #githubRepo: https://github.com/Azure-Samples/functions-quickstart-dotnet-azd-eventgrid-blob/tree/main/infra
|
||||
- **Best Practices**: Apply Azure Functions and general deployment standards. Invoke the `get_bestpractices` tool twice to collect recommendations from both perspectives:
|
||||
- Call with resource = `azurefunctions` and action = `deployment` to get Azure Functions specific deployment best practices.
|
||||
- Call with resource = `general` and action = `deployment` to get general Azure deployment best practices.
|
||||
Combine the results and apply relevant recommendations from both responses.
|
||||
- **Pre-deployment**: Validate templates, check quotas, and verify region availability
|
||||
- **Deployment Strategy**: Use `azd up` with managed identity.
|
||||
- ALWAYS Use Flex Consumption plan (FC1) for deployment, never Y1 dynamic.
|
||||
- ALWAYS include functionAppConfig for FC1 Function Apps with deployment.storage configuration. Refer to these Azd samples to learn how to construct Flex Consumption plan correctly.
|
||||
- #githubRepo: https://github.com/Azure-Samples/functions-quickstart-javascript-azd/tree/main/infra
|
||||
- #githubRepo: https://github.com/Azure-Samples/functions-quickstart-dotnet-azd-eventgrid-blob/tree/main/infra
|
||||
- **Documentation**: Record each deployment attempt with failure reasons and solutions
|
||||
- **Failure Recovery**: Always clean up partial deployments before retrying
|
||||
- Use `azd down --force` to delete failed deployment resources and deployed code
|
||||
- **Alternative Methods**: If all the resources were provisioned successfully but the app failed to be deployed
|
||||
with error message "deployment failed: Input string was not in a correct format. Failure to parse near offset 40.
|
||||
Format item ends prematurely.", use Azure CLI deployment to upload the function app code.
|
||||
|
||||
|
||||
### 6. Post-Deployment
|
||||
- **Authentication**: Retrieve function names being deployed, then retrieve and configure function keys
|
||||
- **Endpoint Testing**: Validate all function endpoints with proper authentication
|
||||
- **Monitoring**: Verify Application Insights telemetry and establish performance baselines
|
||||
- **Documentation**: Create a README with deployment and usage instructions
|
||||
|
||||
## Enterprise Environment Considerations
|
||||
|
||||
### Corporate Policy Compliance
|
||||
- **Alternative Strategies**: Prepare Azure CLI fallback for blocked `azd` commands
|
||||
- **Compliance Standards**: Use Azure Verified Modules (AVM) for enterprise requirements
|
||||
- **Network Restrictions**: Consider VNet integration and private endpoints
|
||||
|
||||
### Security & Authentication
|
||||
- **Managed Identity**: Preferred authentication method for Azure-hosted resources
|
||||
- **Function Keys**: Use function-level keys following principle of least privilege
|
||||
- **Key Management**: Retrieve keys post-deployment for endpoint testing
|
||||
- **RBAC Configuration**: Implement proper role assignments for dependencies
|
||||
|
||||
## Quality Assurance
|
||||
|
||||
### Testing Requirements
|
||||
- **Unit Tests**: 100% passing rate
|
||||
- **Integration Tests**: 80%+ coverage of main scenarios
|
||||
- **Code Quality**: ESLint/linting checks passing
|
||||
- **Performance**: Baseline performance validation
|
||||
|
||||
### Deployment Validation
|
||||
- **Infrastructure**: Bicep templates pass validation
|
||||
- **Pre-deployment**: Use deploy tool and set parameter `command` to be `deploy_iac_rules_get` to get the best practices rules for iac generation.
|
||||
- **Authentication**: Proper managed identity and RBAC configuration
|
||||
- **Monitoring**: Application Insights receiving telemetry
|
||||
|
||||
## Failure Recovery & Troubleshooting
|
||||
|
||||
### Common Issues & Solutions
|
||||
1. **Policy Violations**: Switch to Azure CLI deployment methods
|
||||
2. **Missing Dependencies**: Systematic tool installation and validation
|
||||
3. **Authentication Issues**: Comprehensive RBAC and managed identity setup
|
||||
4. **Runtime Compatibility**: Use supported versions (Node.js 20+, Python 3.11+)
|
||||
5. **Partial Deployments**: Clean resource group deletion before retry
|
||||
|
||||
### Deployment Failure Recovery Protocol
|
||||
```bash
|
||||
# Delete failed deployment resources and deployed code
|
||||
azd down --force
|
||||
|
||||
# Or
|
||||
# Clean failed deployment
|
||||
az group delete --name rg-<AZURE_ENV_NAME> --yes --no-wait
|
||||
az group wait --name rg-<AZURE_ENV_NAME> --deleted --timeout 300
|
||||
|
||||
# Retry deployment
|
||||
azd up
|
||||
```
|
||||
|
||||
## Reference Resources
|
||||
|
||||
### Azure Functions Best Practices
|
||||
- **Programming Models**: Use latest versions (v4 JavaScript, v2 Python)
|
||||
- **Extension Bundles**: Prefer over SDKs for simplified dependency management
|
||||
- **Event Sources**: Use EventGrid for blob triggers
|
||||
- **Configuration**: Generate `local.settings.json` for local development
|
||||
|
||||
### Infrastructure Templates
|
||||
- [JavaScript Azure Functions AZD Sample](https://github.com/Azure-Samples/functions-quickstart-javascript-azd/tree/main/infra)
|
||||
- [.NET Azure Functions with EventGrid Sample](https://github.com/Azure-Samples/functions-quickstart-dotnet-azd-eventgrid-blob/tree/main/infra)
|
||||
@@ -1,249 +0,0 @@
|
||||
name: Production Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
custom_domain:
|
||||
description: 'Custom domain name'
|
||||
required: false
|
||||
default: 'miraclesinmotion.org'
|
||||
force_deploy:
|
||||
description: 'Force deployment even if tests fail'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
env:
|
||||
NODE_VERSION: '22'
|
||||
AZURE_STATIC_WEB_APPS_API_TOKEN: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
||||
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
lfs: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install main dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Install API dependencies
|
||||
run: |
|
||||
cd api
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npx vitest run --reporter=verbose
|
||||
continue-on-error: ${{ github.event.inputs.force_deploy == 'true' }}
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Build API
|
||||
run: |
|
||||
cd api
|
||||
npm run build || npm run tsc
|
||||
cd ..
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-files
|
||||
path: |
|
||||
dist/
|
||||
api/
|
||||
staticwebapp.config.json
|
||||
|
||||
deploy-infrastructure:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-test
|
||||
name: Deploy Infrastructure
|
||||
outputs:
|
||||
static-web-app-name: ${{ steps.deploy.outputs.staticWebAppName }}
|
||||
function-app-name: ${{ steps.deploy.outputs.functionAppName }}
|
||||
static-web-app-url: ${{ steps.deploy.outputs.staticWebAppUrl }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: Create Resource Group
|
||||
run: |
|
||||
az group create \
|
||||
--name rg-miraclesinmotion-prod \
|
||||
--location "East US"
|
||||
|
||||
- name: Deploy Infrastructure
|
||||
id: deploy
|
||||
run: |
|
||||
DEPLOYMENT_NAME="mim-prod-$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
# Deploy infrastructure
|
||||
DEPLOYMENT_OUTPUT=$(az deployment group create \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--template-file infrastructure/main-production.bicep \
|
||||
--parameters infrastructure/main-production.parameters.json \
|
||||
--parameters stripePublicKey="${{ secrets.STRIPE_PUBLIC_KEY }}" \
|
||||
--parameters customDomainName="${{ github.event.inputs.custom_domain || 'miraclesinmotion.org' }}" \
|
||||
--parameters enableCustomDomain=true \
|
||||
--name $DEPLOYMENT_NAME \
|
||||
--output json)
|
||||
|
||||
# Extract outputs
|
||||
STATIC_WEB_APP_NAME=$(echo $DEPLOYMENT_OUTPUT | jq -r '.properties.outputs.staticWebAppName.value')
|
||||
FUNCTION_APP_NAME=$(echo $DEPLOYMENT_OUTPUT | jq -r '.properties.outputs.functionAppName.value')
|
||||
STATIC_WEB_APP_URL=$(echo $DEPLOYMENT_OUTPUT | jq -r '.properties.outputs.staticWebAppUrl.value')
|
||||
|
||||
# Set outputs
|
||||
echo "staticWebAppName=$STATIC_WEB_APP_NAME" >> $GITHUB_OUTPUT
|
||||
echo "functionAppName=$FUNCTION_APP_NAME" >> $GITHUB_OUTPUT
|
||||
echo "staticWebAppUrl=$STATIC_WEB_APP_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✅ Infrastructure deployed successfully"
|
||||
echo "📱 Static Web App: $STATIC_WEB_APP_NAME"
|
||||
echo "⚡ Function App: $FUNCTION_APP_NAME"
|
||||
echo "🌐 URL: $STATIC_WEB_APP_URL"
|
||||
|
||||
deploy-application:
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy-infrastructure
|
||||
name: Deploy Application
|
||||
environment:
|
||||
name: production
|
||||
url: ${{ needs.deploy-infrastructure.outputs.static-web-app-url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-files
|
||||
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: Get Static Web App Deployment Token
|
||||
id: swa-token
|
||||
run: |
|
||||
DEPLOYMENT_TOKEN=$(az staticwebapp secrets list \
|
||||
--name ${{ needs.deploy-infrastructure.outputs.static-web-app-name }} \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--query "properties.apiKey" \
|
||||
--output tsv)
|
||||
echo "::add-mask::$DEPLOYMENT_TOKEN"
|
||||
echo "token=$DEPLOYMENT_TOKEN" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js for SWA CLI
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install SWA CLI
|
||||
run: npm install -g @azure/static-web-apps-cli
|
||||
|
||||
- name: Deploy to Static Web App
|
||||
run: |
|
||||
swa deploy ./dist \
|
||||
--api-location ./api \
|
||||
--env production \
|
||||
--deployment-token ${{ steps.swa-token.outputs.token }}
|
||||
|
||||
- name: Deploy Azure Functions
|
||||
run: |
|
||||
# Create deployment package
|
||||
cd api
|
||||
zip -r ../api-deployment.zip . -x "node_modules/*" "*.test.*" "*.md"
|
||||
cd ..
|
||||
|
||||
# Deploy functions
|
||||
az functionapp deployment source config-zip \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--name ${{ needs.deploy-infrastructure.outputs.function-app-name }} \
|
||||
--src api-deployment.zip
|
||||
|
||||
- name: Warm up application
|
||||
run: |
|
||||
echo "🔥 Warming up the deployed application..."
|
||||
curl -s ${{ needs.deploy-infrastructure.outputs.static-web-app-url }} > /dev/null
|
||||
curl -s ${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/portals > /dev/null
|
||||
echo "✅ Application warmed up successfully"
|
||||
|
||||
post-deployment:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy-infrastructure, deploy-application]
|
||||
name: Post-Deployment Tasks
|
||||
steps:
|
||||
- name: Run smoke tests
|
||||
run: |
|
||||
echo "🧪 Running smoke tests..."
|
||||
|
||||
# Test main page
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" ${{ needs.deploy-infrastructure.outputs.static-web-app-url }})
|
||||
if [ $STATUS -eq 200 ]; then
|
||||
echo "✅ Main page is accessible"
|
||||
else
|
||||
echo "❌ Main page returned status: $STATUS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test portals page
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" ${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/portals)
|
||||
if [ $STATUS -eq 200 ]; then
|
||||
echo "✅ Portals page is accessible"
|
||||
else
|
||||
echo "❌ Portals page returned status: $STATUS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 All smoke tests passed!"
|
||||
|
||||
- name: Create deployment summary
|
||||
run: |
|
||||
echo "## 🚀 Production Deployment Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📊 Deployment Details" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Static Web App**: ${{ needs.deploy-infrastructure.outputs.static-web-app-name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Primary URL**: ${{ needs.deploy-infrastructure.outputs.static-web-app-url }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Portal Access**: ${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/portals" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Custom Domain**: https://${{ github.event.inputs.custom_domain || 'miraclesinmotion.org' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🔗 Quick Links" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [🏠 Main Site](${{ needs.deploy-infrastructure.outputs.static-web-app-url }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [🚪 Portals](${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/portals)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [💰 Donate](${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/donate)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [🤝 Volunteer](${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/volunteers)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [📊 Analytics](${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/analytics)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📋 Next Steps" >> $GITHUB_STEP_SUMMARY
|
||||
echo "1. Configure DNS records for custom domain" >> $GITHUB_STEP_SUMMARY
|
||||
echo "2. Update Stripe webhook endpoints" >> $GITHUB_STEP_SUMMARY
|
||||
echo "3. Test all portal functionality" >> $GITHUB_STEP_SUMMARY
|
||||
echo "4. Monitor application performance" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Notify team
|
||||
if: success()
|
||||
run: |
|
||||
echo "🎉 Production deployment completed successfully!"
|
||||
echo "🌐 Application is live at: ${{ needs.deploy-infrastructure.outputs.static-web-app-url }}"
|
||||
echo "🚪 Portals are accessible at: ${{ needs.deploy-infrastructure.outputs.static-web-app-url }}/#/portals"
|
||||
@@ -76,11 +76,6 @@ jspm_packages/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Azure Functions zip layout: populate from `api/dist` via `npm run build` + copy (docs/deployment). Do not commit tsc emit here.
|
||||
api/deploy-package/**/*.js
|
||||
api/deploy-package/**/*.js.map
|
||||
api/deploy-package/**/*.d.ts
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingSettings": {
|
||||
"isEnabled": true,
|
||||
"excludedTypes": "Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
},
|
||||
"functionTimeout": "00:05:00",
|
||||
"languageWorkers": {
|
||||
"node": {
|
||||
"arguments": ["--max-old-space-size=2048"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "miracles-in-motion-api",
|
||||
"version": "1.0.0",
|
||||
"description": "Azure Functions API for Miracles in Motion nonprofit platform",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"prestart": "npm run build",
|
||||
"start": "func start",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/cosmos": "^4.1.1",
|
||||
"@azure/keyvault-secrets": "^4.8.1",
|
||||
"@azure/identity": "^4.5.0",
|
||||
"@azure/functions": "^4.5.1",
|
||||
"stripe": "^17.3.0",
|
||||
"joi": "^17.13.3",
|
||||
"uuid": "^11.0.3",
|
||||
"cors": "^2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"typescript": "^5.6.3",
|
||||
"jest": "^29.7.0",
|
||||
"@types/jest": "^29.5.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"applicationInsights": {
|
||||
"samplingSettings": {
|
||||
"isEnabled": true,
|
||||
"excludedTypes": "Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
},
|
||||
"functionTimeout": "00:05:00",
|
||||
"languageWorkers": {
|
||||
"node": {
|
||||
"arguments": ["--max-old-space-size=2048"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-4762
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "miracles-in-motion-api",
|
||||
"version": "1.0.0",
|
||||
"description": "Azure Functions API for Miracles in Motion nonprofit platform",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"prestart": "npm run build",
|
||||
"start": "func start",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/cosmos": "^4.1.1",
|
||||
"@azure/keyvault-secrets": "^4.8.1",
|
||||
"@azure/identity": "^4.5.0",
|
||||
"@azure/functions": "^4.5.1",
|
||||
"stripe": "^17.3.0",
|
||||
"joi": "^17.13.3",
|
||||
"uuid": "^11.0.3",
|
||||
"cors": "^2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"typescript": "^5.6.3",
|
||||
"jest": "^29.7.0",
|
||||
"@types/jest": "^29.5.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { CosmosClient } from '@azure/cosmos';
|
||||
import { SecretClient } from '@azure/keyvault-secrets';
|
||||
import { DefaultAzureCredential } from '@azure/identity';
|
||||
import type { Donation } from './types';
|
||||
|
||||
export interface ServiceContainer {
|
||||
donationsContainer: DonationContainerLike;
|
||||
volunteersContainer: DonationContainerLike;
|
||||
programsContainer: DonationContainerLike;
|
||||
secretClient: SecretClientLike;
|
||||
}
|
||||
|
||||
type QueryParameter = { name: string; value: string };
|
||||
|
||||
type DonationQuerySpec = {
|
||||
query: string;
|
||||
parameters?: QueryParameter[];
|
||||
};
|
||||
|
||||
type DonationContainerLike = {
|
||||
items: {
|
||||
create(item: Donation): Promise<void>;
|
||||
query(spec: DonationQuerySpec): {
|
||||
fetchAll(): Promise<{ resources: Donation[] }>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type SecretClientLike = {
|
||||
getSecret(name: string): Promise<{ value?: string }>;
|
||||
};
|
||||
|
||||
class EnvSecretClient implements SecretClientLike {
|
||||
public async getSecret(name: string): Promise<{ value?: string }> {
|
||||
const envName = name.toUpperCase().replace(/-/g, '_');
|
||||
return { value: process.env[envName] };
|
||||
}
|
||||
}
|
||||
|
||||
class FileBackedContainer implements DonationContainerLike {
|
||||
private readonly filePath: string;
|
||||
|
||||
public constructor(collectionName: string) {
|
||||
const dataDir = process.env.MIM_DATA_DIR || path.resolve(process.cwd(), 'data');
|
||||
this.filePath = path.join(dataDir, `${collectionName}.json`);
|
||||
}
|
||||
|
||||
public items = {
|
||||
create: async (item: Donation): Promise<void> => {
|
||||
const items = await this.readAll();
|
||||
items.push(item);
|
||||
await this.writeAll(items);
|
||||
},
|
||||
query: (spec: DonationQuerySpec) => ({
|
||||
fetchAll: async (): Promise<{ resources: Donation[] }> => {
|
||||
let items = await this.readAll();
|
||||
const status = spec.parameters?.find((parameter) => parameter.name === '@status')?.value;
|
||||
const program = spec.parameters?.find((parameter) => parameter.name === '@program')?.value;
|
||||
|
||||
if (status) {
|
||||
items = items.filter((item) => item.status === status);
|
||||
}
|
||||
|
||||
if (program) {
|
||||
items = items.filter((item) => item.program === program);
|
||||
}
|
||||
|
||||
items.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
||||
return { resources: items };
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
private async ensureFile(): Promise<void> {
|
||||
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
||||
|
||||
try {
|
||||
await fs.access(this.filePath);
|
||||
} catch {
|
||||
await fs.writeFile(this.filePath, '[]\n', 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
private async readAll(): Promise<Donation[]> {
|
||||
await this.ensureFile();
|
||||
const raw = await fs.readFile(this.filePath, 'utf8');
|
||||
|
||||
if (!raw.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(raw) as Donation[];
|
||||
}
|
||||
|
||||
private async writeAll(items: Donation[]): Promise<void> {
|
||||
await this.ensureFile();
|
||||
const tempPath = `${this.filePath}.tmp`;
|
||||
await fs.writeFile(tempPath, `${JSON.stringify(items, null, 2)}\n`, 'utf8');
|
||||
await fs.rename(tempPath, this.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
class DIContainer {
|
||||
private static instance: DIContainer;
|
||||
private services: ServiceContainer | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): DIContainer {
|
||||
if (!DIContainer.instance) {
|
||||
DIContainer.instance = new DIContainer();
|
||||
}
|
||||
return DIContainer.instance;
|
||||
}
|
||||
|
||||
public async initializeServices(): Promise<ServiceContainer> {
|
||||
if (this.services) {
|
||||
return this.services;
|
||||
}
|
||||
|
||||
try {
|
||||
const cosmosConnectionString = process.env.COSMOS_CONNECTION_STRING;
|
||||
const keyVaultUrl = process.env.KEY_VAULT_URL;
|
||||
|
||||
let donationsContainer: DonationContainerLike;
|
||||
let volunteersContainer: DonationContainerLike;
|
||||
let programsContainer: DonationContainerLike;
|
||||
|
||||
if (cosmosConnectionString) {
|
||||
const cosmosClient = new CosmosClient(cosmosConnectionString);
|
||||
const databaseName = process.env.COSMOS_DATABASE_NAME || 'MiraclesInMotion';
|
||||
const database = cosmosClient.database(databaseName);
|
||||
|
||||
donationsContainer = database.container('donations') as unknown as DonationContainerLike;
|
||||
volunteersContainer = database.container('volunteers') as unknown as DonationContainerLike;
|
||||
programsContainer = database.container('programs') as unknown as DonationContainerLike;
|
||||
} else {
|
||||
donationsContainer = new FileBackedContainer('donations');
|
||||
volunteersContainer = new FileBackedContainer('volunteers');
|
||||
programsContainer = new FileBackedContainer('programs');
|
||||
}
|
||||
|
||||
const secretClient = keyVaultUrl
|
||||
? new SecretClient(keyVaultUrl, new DefaultAzureCredential())
|
||||
: new EnvSecretClient();
|
||||
|
||||
this.services = {
|
||||
donationsContainer,
|
||||
volunteersContainer,
|
||||
programsContainer,
|
||||
secretClient
|
||||
};
|
||||
|
||||
console.log(
|
||||
`Services initialized successfully (${cosmosConnectionString ? 'cosmos' : 'local-file'} storage, ${keyVaultUrl ? 'key-vault' : 'env'} secrets)`
|
||||
);
|
||||
return this.services;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize services:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public getServices(): ServiceContainer {
|
||||
if (!this.services) {
|
||||
throw new Error('Services not initialized. Call initializeServices() first.');
|
||||
}
|
||||
return this.services;
|
||||
}
|
||||
}
|
||||
|
||||
export default DIContainer;
|
||||
@@ -1,181 +0,0 @@
|
||||
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
||||
|
||||
const DEFAULT_MODEL = 'grok-3';
|
||||
const DEFAULT_BASE_URL = 'https://api.x.ai/v1';
|
||||
const MAX_MESSAGES = 10;
|
||||
const MAX_MESSAGE_CHARS = 4000;
|
||||
|
||||
type ChatRole = 'system' | 'assistant' | 'user';
|
||||
|
||||
interface ChatMessage {
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ChatRequestBody {
|
||||
messages?: ChatMessage[];
|
||||
pageContext?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
interface xAIChatResponse {
|
||||
choices?: Array<{
|
||||
message?: {
|
||||
content?: string;
|
||||
};
|
||||
}>;
|
||||
output_text?: string;
|
||||
output?: Array<{
|
||||
content?: Array<{
|
||||
text?: string;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
function jsonResponse(status: number, body: Record<string, unknown>): HttpResponseInit {
|
||||
return {
|
||||
status,
|
||||
jsonBody: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMessages(messages: ChatMessage[] | undefined): ChatMessage[] {
|
||||
if (!Array.isArray(messages)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return messages
|
||||
.filter((message): message is ChatMessage => Boolean(message && typeof message === 'object'))
|
||||
.map(message => ({
|
||||
role: message.role,
|
||||
content: String(message.content || '').trim().slice(0, MAX_MESSAGE_CHARS)
|
||||
}))
|
||||
.filter(message => ['assistant', 'user', 'system'].includes(message.role) && message.content.length > 0)
|
||||
.slice(-MAX_MESSAGES);
|
||||
}
|
||||
|
||||
function buildSystemPrompt(pagePath: string): string {
|
||||
return [
|
||||
'You are the Miracles in Motion website assistant for MIM4U.',
|
||||
'Answer as a warm, concise concierge for Miracles in Motion Foundation — a faith-centered nonprofit serving Los Angeles County with outreach, emergency assistance, resource navigation, wellness support, volunteer coordination, donations, and corporate partnerships.',
|
||||
'Use only the provided context and the user messages. If you are unsure, say so and direct the user to [email protected].',
|
||||
'Keep replies practical and short. Prefer bullets only when they improve clarity.',
|
||||
'When relevant, guide users to these routes: #/donate, #/volunteers, #/request-assistance, #/impact, #/stories.',
|
||||
'Do not invent phone numbers, addresses, office hours, eligibility rules, or application steps.',
|
||||
'If someone needs urgent support, tell them to use #/request-assistance and contact [email protected] directly.',
|
||||
`Current page route: ${pagePath || '/'}`
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function extractReply(payload: xAIChatResponse): string {
|
||||
const choiceReply = payload.choices?.[0]?.message?.content?.trim();
|
||||
if (choiceReply) {
|
||||
return choiceReply;
|
||||
}
|
||||
|
||||
const outputText = payload.output_text?.trim();
|
||||
if (outputText) {
|
||||
return outputText;
|
||||
}
|
||||
|
||||
const outputReply = payload.output
|
||||
?.flatMap(item => item.content ?? [])
|
||||
.map(item => item.text?.trim() ?? '')
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
return outputReply || '';
|
||||
}
|
||||
|
||||
export async function chatWithAssistant(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
||||
if (request.method === 'OPTIONS') {
|
||||
return jsonResponse(200, { ok: true });
|
||||
}
|
||||
|
||||
const apiKey = process.env.XAI_API_KEY?.trim();
|
||||
if (!apiKey) {
|
||||
return jsonResponse(503, {
|
||||
success: false,
|
||||
error: 'Assistant is not configured on the server.',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json() as ChatRequestBody;
|
||||
const messages = normalizeMessages(body.messages);
|
||||
if (messages.length === 0) {
|
||||
return jsonResponse(400, {
|
||||
success: false,
|
||||
error: 'At least one message is required.',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const pagePath = body.pageContext?.path || '/';
|
||||
const model = process.env.XAI_MODEL?.trim() || process.env.EXPLORER_AI_MODEL?.trim() || DEFAULT_MODEL;
|
||||
const baseUrl = (process.env.XAI_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
||||
|
||||
const upstreamResponse = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
stream: false,
|
||||
messages: [
|
||||
{ role: 'system', content: buildSystemPrompt(pagePath) },
|
||||
...messages
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const payload = await upstreamResponse.json() as xAIChatResponse;
|
||||
if (!upstreamResponse.ok) {
|
||||
context.error('xAI upstream returned an error', upstreamResponse.status, payload);
|
||||
return jsonResponse(502, {
|
||||
success: false,
|
||||
error: 'Assistant upstream request failed.',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const reply = extractReply(payload);
|
||||
if (!reply) {
|
||||
return jsonResponse(502, {
|
||||
success: false,
|
||||
error: 'Assistant returned an empty response.',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse(200, {
|
||||
success: true,
|
||||
reply,
|
||||
model,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
context.error('Error in ai chat handler:', error);
|
||||
return jsonResponse(500, {
|
||||
success: false,
|
||||
error: 'Assistant request failed.',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
app.http('chatWithAssistant', {
|
||||
methods: ['POST', 'OPTIONS'],
|
||||
authLevel: 'anonymous',
|
||||
route: 'ai/chat',
|
||||
handler: chatWithAssistant
|
||||
});
|
||||
@@ -1,139 +0,0 @@
|
||||
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
||||
import DIContainer from '../DIContainer';
|
||||
import { ApiResponse, CreateDonationRequest, Donation } from '../types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export async function createDonation(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
||||
try {
|
||||
await DIContainer.getInstance().initializeServices();
|
||||
const { donationsContainer, secretClient } = DIContainer.getInstance().getServices();
|
||||
|
||||
// Get request body
|
||||
const donationRequest = await request.json() as CreateDonationRequest;
|
||||
|
||||
// Validate required fields
|
||||
if (!donationRequest.amount || !donationRequest.donorEmail || !donationRequest.donorName) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: 'Missing required fields: amount, donorEmail, donorName',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return {
|
||||
status: 400,
|
||||
jsonBody: response,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize Stripe if payment method is stripe
|
||||
let stripePaymentIntentId: string | undefined;
|
||||
if (donationRequest.paymentMethod === 'stripe') {
|
||||
try {
|
||||
const stripeSecretKey = await secretClient.getSecret('stripe-secret-key');
|
||||
if (!stripeSecretKey.value) {
|
||||
throw new Error('stripe-secret-key is not configured');
|
||||
}
|
||||
|
||||
const stripe = new Stripe(stripeSecretKey.value!, {
|
||||
apiVersion: '2025-02-24.acacia'
|
||||
});
|
||||
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
amount: Math.round(donationRequest.amount * 100), // Convert to cents
|
||||
currency: donationRequest.currency.toLowerCase(),
|
||||
metadata: {
|
||||
donorEmail: donationRequest.donorEmail,
|
||||
donorName: donationRequest.donorName,
|
||||
program: donationRequest.program || 'general'
|
||||
}
|
||||
});
|
||||
|
||||
stripePaymentIntentId = paymentIntent.id;
|
||||
} catch (stripeError) {
|
||||
context.error('Stripe payment intent creation failed:', stripeError);
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: 'Payment processing failed',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
jsonBody: response,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create donation record
|
||||
const donation: Donation = {
|
||||
id: uuidv4(),
|
||||
amount: donationRequest.amount,
|
||||
currency: donationRequest.currency,
|
||||
donorName: donationRequest.donorName,
|
||||
donorEmail: donationRequest.donorEmail,
|
||||
donorPhone: donationRequest.donorPhone,
|
||||
program: donationRequest.program,
|
||||
isRecurring: donationRequest.isRecurring,
|
||||
frequency: donationRequest.frequency,
|
||||
paymentMethod: donationRequest.paymentMethod,
|
||||
stripePaymentIntentId,
|
||||
status: 'pending',
|
||||
message: donationRequest.message,
|
||||
isAnonymous: donationRequest.isAnonymous,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Persist the donation using the active backend.
|
||||
await donationsContainer.items.create(donation);
|
||||
|
||||
const response: ApiResponse<Donation> = {
|
||||
success: true,
|
||||
data: donation,
|
||||
message: 'Donation created successfully',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
jsonBody: response,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
context.error('Error creating donation:', error);
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to create donation',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
jsonBody: response,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
app.http('createDonation', {
|
||||
methods: ['POST'],
|
||||
authLevel: 'anonymous',
|
||||
route: 'donations',
|
||||
handler: createDonation
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
||||
import DIContainer from '../DIContainer';
|
||||
import { ApiResponse, PaginatedResponse, Donation } from '../types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export async function getDonations(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
||||
try {
|
||||
await DIContainer.getInstance().initializeServices();
|
||||
const { donationsContainer } = DIContainer.getInstance().getServices();
|
||||
|
||||
const page = parseInt(request.query.get('page') || '1');
|
||||
const limit = parseInt(request.query.get('limit') || '10');
|
||||
const status = request.query.get('status');
|
||||
const program = request.query.get('program');
|
||||
|
||||
let query = 'SELECT * FROM c WHERE 1=1';
|
||||
const parameters: any[] = [];
|
||||
|
||||
if (status) {
|
||||
query += ' AND c.status = @status';
|
||||
parameters.push({ name: '@status', value: status });
|
||||
}
|
||||
|
||||
if (program) {
|
||||
query += ' AND c.program = @program';
|
||||
parameters.push({ name: '@program', value: program });
|
||||
}
|
||||
|
||||
query += ' ORDER BY c.createdAt DESC';
|
||||
|
||||
const { resources: donations } = await donationsContainer.items
|
||||
.query({
|
||||
query,
|
||||
parameters
|
||||
})
|
||||
.fetchAll();
|
||||
|
||||
// Simple pagination
|
||||
const total = donations.length;
|
||||
const pages = Math.ceil(total / limit);
|
||||
const startIndex = (page - 1) * limit;
|
||||
const endIndex = startIndex + limit;
|
||||
const paginatedDonations = donations.slice(startIndex, endIndex);
|
||||
|
||||
const response: PaginatedResponse<Donation> = {
|
||||
success: true,
|
||||
data: paginatedDonations,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
jsonBody: response,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
context.error('Error fetching donations:', error);
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: 'Failed to fetch donations',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
jsonBody: response,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
app.http('getDonations', {
|
||||
methods: ['GET'],
|
||||
authLevel: 'anonymous',
|
||||
route: 'donations',
|
||||
handler: getDonations
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import './donations/createDonation';
|
||||
import './donations/getDonations';
|
||||
import './ai/chat';
|
||||
@@ -1,180 +0,0 @@
|
||||
export interface Donation {
|
||||
id: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
donorName: string;
|
||||
donorEmail: string;
|
||||
donorPhone?: string;
|
||||
program?: string;
|
||||
isRecurring: boolean;
|
||||
frequency?: 'monthly' | 'quarterly' | 'annually';
|
||||
paymentMethod: 'stripe' | 'paypal' | 'bank_transfer';
|
||||
stripePaymentIntentId?: string;
|
||||
status: 'pending' | 'completed' | 'failed' | 'cancelled' | 'refunded';
|
||||
message?: string;
|
||||
isAnonymous: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Volunteer {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
dateOfBirth: string;
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
};
|
||||
emergencyContact: {
|
||||
name: string;
|
||||
phone: string;
|
||||
relationship: string;
|
||||
};
|
||||
skills: string[];
|
||||
interests: string[];
|
||||
availability: {
|
||||
monday: boolean;
|
||||
tuesday: boolean;
|
||||
wednesday: boolean;
|
||||
thursday: boolean;
|
||||
friday: boolean;
|
||||
saturday: boolean;
|
||||
sunday: boolean;
|
||||
timeSlots: string[];
|
||||
};
|
||||
experience: string;
|
||||
motivation: string;
|
||||
backgroundCheck: {
|
||||
completed: boolean;
|
||||
completedDate?: string;
|
||||
status?: 'pending' | 'approved' | 'rejected';
|
||||
};
|
||||
status: 'pending' | 'approved' | 'inactive' | 'suspended';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastActivityAt?: string;
|
||||
}
|
||||
|
||||
export interface Program {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'education' | 'healthcare' | 'community' | 'environment' | 'arts' | 'other';
|
||||
targetAudience: string;
|
||||
goals: string[];
|
||||
location: {
|
||||
type: 'physical' | 'virtual' | 'hybrid';
|
||||
address?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
virtualLink?: string;
|
||||
};
|
||||
schedule: {
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
frequency: 'one-time' | 'weekly' | 'monthly' | 'ongoing';
|
||||
daysOfWeek: string[];
|
||||
timeSlots: string[];
|
||||
};
|
||||
requirements: {
|
||||
minimumAge?: number;
|
||||
maximumAge?: number;
|
||||
skills?: string[];
|
||||
experience?: string;
|
||||
other?: string[];
|
||||
};
|
||||
capacity: {
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
current: number;
|
||||
};
|
||||
budget: {
|
||||
total: number;
|
||||
raised: number;
|
||||
currency: string;
|
||||
};
|
||||
coordinator: {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
};
|
||||
volunteers: string[]; // Array of volunteer IDs
|
||||
status: 'planning' | 'active' | 'completed' | 'cancelled' | 'on-hold';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
images?: string[];
|
||||
documents?: string[];
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
pages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateDonationRequest {
|
||||
amount: number;
|
||||
currency: string;
|
||||
donorName: string;
|
||||
donorEmail: string;
|
||||
donorPhone?: string;
|
||||
program?: string;
|
||||
isRecurring: boolean;
|
||||
frequency?: 'monthly' | 'quarterly' | 'annually';
|
||||
paymentMethod: 'stripe' | 'paypal' | 'bank_transfer';
|
||||
message?: string;
|
||||
isAnonymous: boolean;
|
||||
}
|
||||
|
||||
export interface CreateVolunteerRequest {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
dateOfBirth: string;
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
};
|
||||
emergencyContact: {
|
||||
name: string;
|
||||
phone: string;
|
||||
relationship: string;
|
||||
};
|
||||
skills: string[];
|
||||
interests: string[];
|
||||
availability: {
|
||||
monday: boolean;
|
||||
tuesday: boolean;
|
||||
wednesday: boolean;
|
||||
thursday: boolean;
|
||||
friday: boolean;
|
||||
saturday: boolean;
|
||||
sunday: boolean;
|
||||
timeSlots: string[];
|
||||
};
|
||||
experience: string;
|
||||
motivation: string;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.test.ts", "dist"]
|
||||
}
|
||||
@@ -1,768 +1,57 @@
|
||||
# 🚀 Deployment Prerequisites Guide
|
||||
# Deployment prerequisites (Proxmox production)
|
||||
|
||||
Complete guide for setting up MS Azure, MS Entra, Cloudflare, and all other services required for production deployment.
|
||||
> **Azure removed (2026-06):** Static Web Apps, Bicep infra, Azure Functions (`api/`), and Azure CI workflows are no longer part of this repo. Production is **VMID 7810** via Proxmox — see [PROXMOX_DEPLOYMENT.md](./deployment/PROXMOX_DEPLOYMENT.md).
|
||||
|
||||
## 📋 Table of Contents
|
||||
## 1. Operator / LAN access
|
||||
|
||||
1. [Azure Setup](#azure-setup)
|
||||
2. [MS Entra (Azure AD) Configuration](#ms-entra-azure-ad-configuration)
|
||||
3. [Cloudflare Configuration](#cloudflare-configuration)
|
||||
4. [Stripe Configuration](#stripe-configuration)
|
||||
5. [Environment Variables](#environment-variables)
|
||||
6. [Pre-Deployment Checklist](#pre-deployment-checklist)
|
||||
7. [Post-Deployment Verification](#post-deployment-verification)
|
||||
| Requirement | Detail |
|
||||
|-------------|--------|
|
||||
| Proxmox SSH | `[email protected]` (r630-02) |
|
||||
| Web CT | VMID **7810** @ `192.168.11.37` |
|
||||
| API CT | VMID **7811** @ `192.168.11.36` (optional; `/api/` proxy) |
|
||||
| Deploy script | `../proxmox/scripts/mim4u-deploy-to-7810.sh` |
|
||||
|
||||
---
|
||||
|
||||
## 1. Azure Setup
|
||||
|
||||
### 1.1 Prerequisites
|
||||
|
||||
- Azure subscription with Contributor or Owner role
|
||||
- Azure CLI installed and configured
|
||||
- Bicep CLI installed (optional, for local validation)
|
||||
- PowerShell 7+ (for deployment scripts)
|
||||
|
||||
### 1.2 Initial Azure Configuration
|
||||
|
||||
#### Login to Azure
|
||||
## 2. Build toolchain
|
||||
|
||||
```bash
|
||||
# Login to Azure
|
||||
az login
|
||||
|
||||
# Verify subscription
|
||||
az account show
|
||||
|
||||
# Set default subscription (if multiple)
|
||||
az account set --subscription "Your Subscription ID"
|
||||
node -v # 22.x recommended
|
||||
npm -v # 10+
|
||||
convert -version # ImageMagick (brand prebuild)
|
||||
```
|
||||
|
||||
#### Create Resource Group
|
||||
Install deps:
|
||||
|
||||
```bash
|
||||
# Create resource group for production
|
||||
az group create \
|
||||
--name rg-miraclesinmotion-prod \
|
||||
--location eastus2
|
||||
|
||||
# Verify resource group
|
||||
az group show --name rg-miraclesinmotion-prod
|
||||
cd miracles_in_motion
|
||||
npm ci # uses legacy-peer-deps via .npmrc
|
||||
```
|
||||
|
||||
### 1.3 Required Azure Services
|
||||
## 3. Environment variables
|
||||
|
||||
The infrastructure deployment will create:
|
||||
Copy `.env.example` → `.env.local` for local dev. Production frontend is static; secrets belong on **7811** (API) or NPMplus, not in the Vite bundle.
|
||||
|
||||
- **Azure Static Web Apps** (Standard SKU) - Frontend hosting
|
||||
- **Azure Functions** (Premium EP1) - Backend API
|
||||
- **Azure Cosmos DB** - Database
|
||||
- **Azure Key Vault** - Secrets management
|
||||
- **Azure Application Insights** - Monitoring
|
||||
- **Log Analytics Workspace** - Logging
|
||||
- **Azure SignalR** - Real-time communications
|
||||
- **Storage Account** - Function app storage
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `VITE_STRIPE_PUBLISHABLE_KEY` | Donate page (public key only) |
|
||||
| `VITE_API_BASE_URL` | Override API base (default `/api` via nginx) |
|
||||
| `VITE_GA_MEASUREMENT_ID` | Analytics (optional) |
|
||||
|
||||
### 1.4 Deploy Infrastructure
|
||||
See `env.production.example` for a non-Azure production template.
|
||||
|
||||
```bash
|
||||
# Navigate to infrastructure directory
|
||||
cd infrastructure
|
||||
## 4. Pre-deploy checklist
|
||||
|
||||
# Deploy production infrastructure
|
||||
az deployment group create \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--template-file main-production.bicep \
|
||||
--parameters main-production.parameters.json \
|
||||
--parameters stripePublicKey="pk_live_YOUR_KEY" \
|
||||
--parameters customDomainName="miraclesinmotion.org" \
|
||||
--parameters enableCustomDomain=true
|
||||
- [ ] `npm run validate:ci` passes (type-check + tests + header WCAG audit)
|
||||
- [ ] `npm run build` succeeds (brand export + Vite)
|
||||
- [ ] NPMplus TLS valid for mim4u.org / www / secure / training
|
||||
- [ ] `./scripts/mim4u-deploy-to-7810.sh` from proxmox repo (or `--dry-run` review)
|
||||
- [ ] Post-deploy: `curl -H 'Host: mim4u.org' http://192.168.11.37/` → 200
|
||||
- [ ] Optional: `npm run a11y:header-audit:live` on production URL
|
||||
|
||||
# Note: Replace pk_live_YOUR_KEY with your actual Stripe public key
|
||||
```
|
||||
## 5. Stripe & DNS (unchanged)
|
||||
|
||||
### 1.5 Get Deployment Outputs
|
||||
- **Stripe:** live keys and webhooks configured on API (7811), not in this static repo.
|
||||
- **Cloudflare / DNS:** A/AAAA or CNAME to public IP per NPMplus; use proxmox `scripts/update-all-dns-to-public-ip.sh --zone-only=mim4u.org --dry-run` before changes.
|
||||
|
||||
```bash
|
||||
# Get deployment outputs
|
||||
az deployment group show \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--name deployment-name \
|
||||
--query properties.outputs
|
||||
```
|
||||
|
||||
**Important Outputs:**
|
||||
- `staticWebAppName` - Static Web App resource name
|
||||
- `staticWebAppUrl` - Default URL for Static Web App
|
||||
- `functionAppName` - Function App resource name
|
||||
- `keyVaultName` - Key Vault resource name
|
||||
- `appInsightsName` - Application Insights resource name
|
||||
|
||||
---
|
||||
|
||||
## 2. MS Entra (Azure AD) Configuration
|
||||
|
||||
### 2.1 Create App Registration
|
||||
|
||||
#### Using Azure Portal
|
||||
|
||||
1. Navigate to **Azure Portal** → **Microsoft Entra ID** → **App registrations**
|
||||
2. Click **+ New registration**
|
||||
3. Configure:
|
||||
- **Name**: `Miracles In Motion Web App`
|
||||
- **Supported account types**: `Accounts in any organizational directory and personal Microsoft accounts`
|
||||
- **Redirect URI**:
|
||||
- Type: `Single-page application (SPA)`
|
||||
- URI: `https://miraclesinmotion.org` (production)
|
||||
- URI: `https://YOUR_STATIC_WEB_APP.azurestaticapps.net` (staging)
|
||||
|
||||
4. Click **Register**
|
||||
|
||||
#### Using Azure CLI
|
||||
|
||||
```bash
|
||||
# Create app registration
|
||||
az ad app create \
|
||||
--display-name "Miracles In Motion Web App" \
|
||||
--sign-in-audience "AzureADMultipleOrgs" \
|
||||
--web-redirect-uris "https://miraclesinmotion.org" "https://www.miraclesinmotion.org"
|
||||
|
||||
# Get app registration ID
|
||||
APP_ID=$(az ad app list --display-name "Miracles In Motion Web App" --query "[0].appId" -o tsv)
|
||||
echo "App ID: $APP_ID"
|
||||
```
|
||||
|
||||
### 2.2 Configure Authentication
|
||||
|
||||
1. In the app registration, go to **Authentication**
|
||||
2. Enable **ID tokens** (used for implicit and hybrid flows)
|
||||
3. Add redirect URIs:
|
||||
- `https://miraclesinmotion.org`
|
||||
- `https://www.miraclesinmotion.org`
|
||||
- `https://YOUR_STATIC_WEB_APP.azurestaticapps.net`
|
||||
4. Under **Implicit grant and hybrid flows**, enable:
|
||||
- ✅ ID tokens
|
||||
5. Save changes
|
||||
|
||||
### 2.3 Configure API Permissions
|
||||
|
||||
1. Go to **API permissions**
|
||||
2. Click **+ Add a permission**
|
||||
3. Select **Microsoft Graph**
|
||||
4. Add the following **Delegated permissions**:
|
||||
- `User.Read` - Read user profile
|
||||
- `User.ReadBasic.All` - Read all users' basic profiles
|
||||
- `email` - View users' email address
|
||||
- `openid` - Sign users in
|
||||
- `profile` - View users' basic profile
|
||||
5. Click **Add permissions**
|
||||
6. Click **Grant admin consent** (if you have admin rights)
|
||||
|
||||
### 2.4 Create Client Secret (Optional - for server-side flows)
|
||||
|
||||
```bash
|
||||
# Create client secret (valid for 24 months)
|
||||
az ad app credential reset \
|
||||
--id $APP_ID \
|
||||
--display-name "Miracles In Motion Secret" \
|
||||
--years 2
|
||||
|
||||
# Save the secret value immediately - it won't be shown again!
|
||||
```
|
||||
|
||||
### 2.5 Configure App Roles
|
||||
|
||||
1. Go to **App roles** → **+ Create app role**
|
||||
2. Create roles:
|
||||
- **Display name**: `Admin`
|
||||
- **Allowed member types**: `Users/Groups`
|
||||
- **Value**: `Admin`
|
||||
- **Description**: `Administrator access to all features`
|
||||
|
||||
- **Display name**: `Volunteer`
|
||||
- **Allowed member types**: `Users/Groups`
|
||||
- **Value**: `Volunteer`
|
||||
- **Description**: `Volunteer access to assigned tasks`
|
||||
|
||||
- **Display name**: `Resource`
|
||||
- **Allowed member types**: `Users/Groups`
|
||||
- **Value**: `Resource`
|
||||
- **Description**: `Resource provider access`
|
||||
|
||||
3. Save each role
|
||||
|
||||
### 2.6 Assign Users to Roles
|
||||
|
||||
```bash
|
||||
# Get user object ID
|
||||
USER_ID=$(az ad user show --id "[email protected]" --query "id" -o tsv)
|
||||
|
||||
# Get app role ID (Admin role)
|
||||
ROLE_ID=$(az ad app show --id $APP_ID --query "appRoles[?value=='Admin'].id" -o tsv)
|
||||
|
||||
# Assign user to role
|
||||
az ad app assignment create \
|
||||
--app-id $APP_ID \
|
||||
--principal-id $USER_ID \
|
||||
--role-id $ROLE_ID
|
||||
```
|
||||
|
||||
### 2.7 Configure Static Web App Authentication
|
||||
|
||||
1. Navigate to **Static Web App** → **Authentication**
|
||||
2. Click **Add identity provider**
|
||||
3. Select **Microsoft**
|
||||
4. Configure:
|
||||
- **App registration**: Select your app registration
|
||||
- **App ID**: Your app registration ID
|
||||
- **App secret setting name**: `MICROSOFT_CLIENT_SECRET` (optional)
|
||||
5. Save
|
||||
|
||||
#### Using Azure CLI
|
||||
|
||||
```bash
|
||||
# Get Static Web App resource ID
|
||||
SWA_ID=$(az staticwebapp show \
|
||||
--name YOUR_STATIC_WEB_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--query "id" -o tsv)
|
||||
|
||||
# Configure Microsoft identity provider
|
||||
az staticwebapp identity assign \
|
||||
--name YOUR_STATIC_WEB_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod
|
||||
|
||||
# Note: Static Web Apps authentication is configured via Azure Portal
|
||||
# or through the staticwebapp.config.json file
|
||||
```
|
||||
|
||||
### 2.8 Update staticwebapp.config.json
|
||||
|
||||
The `staticwebapp.config.json` file should include authentication configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"route": "/api/*",
|
||||
"allowedRoles": ["anonymous", "authenticated"]
|
||||
},
|
||||
{
|
||||
"route": "/admin/*",
|
||||
"allowedRoles": ["Admin"]
|
||||
},
|
||||
{
|
||||
"route": "/volunteer/*",
|
||||
"allowedRoles": ["Volunteer", "Admin"]
|
||||
},
|
||||
{
|
||||
"route": "/*",
|
||||
"rewrite": "/index.html"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"identityProviders": {
|
||||
"azureActiveDirectory": {
|
||||
"registration": {
|
||||
"openIdIssuer": "https://login.microsoftonline.com/{tenantId}/v2.0",
|
||||
"clientIdSettingName": "AZURE_CLIENT_ID",
|
||||
"clientSecretSettingName": "AZURE_CLIENT_SECRET"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"navigationFallback": {
|
||||
"rewrite": "/index.html",
|
||||
"exclude": ["/api/*", "/admin/*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.9 Store Configuration in Key Vault
|
||||
|
||||
```bash
|
||||
# Store Azure AD configuration in Key Vault
|
||||
az keyvault secret set \
|
||||
--vault-name YOUR_KEY_VAULT_NAME \
|
||||
--name "azure-client-id" \
|
||||
--value "$APP_ID"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name YOUR_KEY_VAULT_NAME \
|
||||
--name "azure-client-secret" \
|
||||
--value "YOUR_CLIENT_SECRET"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name YOUR_KEY_VAULT_NAME \
|
||||
--name "azure-tenant-id" \
|
||||
--value "$(az account show --query tenantId -o tsv)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Cloudflare Configuration
|
||||
|
||||
### 3.1 Prerequisites
|
||||
|
||||
- Cloudflare account
|
||||
- Domain registered and added to Cloudflare
|
||||
- DNS management access
|
||||
|
||||
### 3.2 Add Domain to Cloudflare
|
||||
|
||||
1. Log in to **Cloudflare Dashboard**
|
||||
2. Click **Add a site**
|
||||
3. Enter your domain: `miraclesinmotion.org`
|
||||
4. Select a plan (Free plan is sufficient for basic needs)
|
||||
5. Cloudflare will scan your existing DNS records
|
||||
|
||||
### 3.3 Update Nameservers
|
||||
|
||||
1. Copy the nameservers provided by Cloudflare
|
||||
2. Update your domain registrar with these nameservers
|
||||
3. Wait for DNS propagation (24-48 hours)
|
||||
|
||||
### 3.4 Configure DNS Records
|
||||
|
||||
#### Add CNAME Records
|
||||
|
||||
1. Go to **DNS** → **Records**
|
||||
2. Add the following records:
|
||||
|
||||
| Type | Name | Content | Proxy | TTL |
|
||||
|------|------|---------|-------|-----|
|
||||
| CNAME | www | YOUR_STATIC_WEB_APP.azurestaticapps.net | ✅ Proxied | Auto |
|
||||
| CNAME | @ | YOUR_STATIC_WEB_APP.azurestaticapps.net | ✅ Proxied | Auto |
|
||||
|
||||
**Note**: Replace `YOUR_STATIC_WEB_APP` with your actual Static Web App name.
|
||||
|
||||
#### Verify DNS Configuration
|
||||
|
||||
```bash
|
||||
# Check DNS records
|
||||
dig miraclesinmotion.org
|
||||
dig www.miraclesinmotion.org
|
||||
|
||||
# Check Cloudflare proxy status
|
||||
curl -I https://miraclesinmotion.org
|
||||
# Look for "CF-Cache-Status" header
|
||||
```
|
||||
|
||||
### 3.5 Configure SSL/TLS
|
||||
|
||||
1. Go to **SSL/TLS** → **Overview**
|
||||
2. Select **Full (strict)** encryption mode
|
||||
3. Enable **Always Use HTTPS**
|
||||
4. Enable **Automatic HTTPS Rewrites**
|
||||
|
||||
### 3.6 Configure Page Rules
|
||||
|
||||
1. Go to **Rules** → **Page Rules**
|
||||
2. Create rules:
|
||||
|
||||
**Rule 1: Force HTTPS**
|
||||
- URL: `*miraclesinmotion.org/*`
|
||||
- Settings:
|
||||
- Always Use HTTPS: ✅ On
|
||||
- SSL: Full (strict)
|
||||
|
||||
**Rule 2: Cache Static Assets**
|
||||
- URL: `*miraclesinmotion.org/assets/*`
|
||||
- Settings:
|
||||
- Cache Level: Cache Everything
|
||||
- Edge Cache TTL: 1 month
|
||||
|
||||
### 3.7 Configure Security Settings
|
||||
|
||||
1. Go to **Security** → **Settings**
|
||||
2. Configure:
|
||||
- **Security Level**: Medium
|
||||
- **Challenge Passage**: 30 minutes
|
||||
- **Browser Integrity Check**: On
|
||||
- **Privacy Pass Support**: On
|
||||
|
||||
### 3.8 Configure Firewall Rules
|
||||
|
||||
1. Go to **Security** → **WAF** → **Custom rules**
|
||||
2. Create rules to block malicious traffic:
|
||||
|
||||
**Rule: Block Bad Bots**
|
||||
- Expression: `(http.user_agent contains "bot" and not http.user_agent contains "Googlebot")`
|
||||
- Action: Block
|
||||
|
||||
**Rule: Rate Limiting**
|
||||
- Expression: `(http.request.uri.path contains "/api/")`
|
||||
- Action: Challenge
|
||||
- Rate: 100 requests per minute
|
||||
|
||||
### 3.9 Configure Speed Optimization
|
||||
|
||||
1. Go to **Speed** → **Optimization**
|
||||
2. Enable:
|
||||
- ✅ Auto Minify (JavaScript, CSS, HTML)
|
||||
- ✅ Brotli compression
|
||||
- ✅ Rocket Loader (optional)
|
||||
- ✅ Mirage (optional, for mobile)
|
||||
|
||||
### 3.10 Configure Analytics
|
||||
|
||||
1. Go to **Analytics** → **Web Analytics**
|
||||
2. Enable **Web Analytics** for your domain
|
||||
3. Add the tracking script to your application (optional)
|
||||
|
||||
### 3.11 Configure Custom Domain in Azure
|
||||
|
||||
After DNS is configured:
|
||||
|
||||
```bash
|
||||
# Add custom domain to Static Web App
|
||||
az staticwebapp hostname set \
|
||||
--name YOUR_STATIC_WEB_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--hostname "miraclesinmotion.org"
|
||||
|
||||
az staticwebapp hostname set \
|
||||
--name YOUR_STATIC_WEB_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--hostname "www.miraclesinmotion.org"
|
||||
```
|
||||
|
||||
**Note**: Azure will automatically provision SSL certificates for custom domains.
|
||||
|
||||
### 3.12 Verify Cloudflare Configuration
|
||||
|
||||
```bash
|
||||
# Test DNS resolution
|
||||
nslookup miraclesinmotion.org
|
||||
nslookup www.miraclesinmotion.org
|
||||
|
||||
# Test HTTPS
|
||||
curl -I https://miraclesinmotion.org
|
||||
|
||||
# Test Cloudflare headers
|
||||
curl -I https://miraclesinmotion.org | grep -i "cf-"
|
||||
|
||||
# Expected headers:
|
||||
# CF-Cache-Status: DYNAMIC
|
||||
# CF-Ray: [unique-id]
|
||||
# Server: cloudflare
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Stripe Configuration
|
||||
|
||||
### 4.1 Create Stripe Account
|
||||
|
||||
1. Go to [Stripe Dashboard](https://dashboard.stripe.com)
|
||||
2. Create account or log in
|
||||
3. Complete account verification
|
||||
|
||||
### 4.2 Get API Keys
|
||||
|
||||
1. Go to **Developers** → **API keys**
|
||||
2. Copy:
|
||||
- **Publishable key** (starts with `pk_live_`)
|
||||
- **Secret key** (starts with `sk_live_`) - Keep this secret!
|
||||
|
||||
### 4.3 Configure Webhooks
|
||||
|
||||
1. Go to **Developers** → **Webhooks**
|
||||
2. Click **+ Add endpoint**
|
||||
3. Configure:
|
||||
- **Endpoint URL**: `https://miraclesinmotion.org/api/webhooks/stripe`
|
||||
- **Events to send**: Select relevant events:
|
||||
- `payment_intent.succeeded`
|
||||
- `payment_intent.payment_failed`
|
||||
- `charge.succeeded`
|
||||
- `charge.failed`
|
||||
4. Copy the **Webhook signing secret** (starts with `whsec_`)
|
||||
|
||||
### 4.4 Store Stripe Secrets in Key Vault
|
||||
|
||||
```bash
|
||||
# Store Stripe keys in Key Vault
|
||||
az keyvault secret set \
|
||||
--vault-name YOUR_KEY_VAULT_NAME \
|
||||
--name "stripe-publishable-key" \
|
||||
--value "pk_live_YOUR_KEY"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name YOUR_KEY_VAULT_NAME \
|
||||
--name "stripe-secret-key" \
|
||||
--value "sk_live_YOUR_KEY"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name YOUR_KEY_VAULT_NAME \
|
||||
--name "stripe-webhook-secret" \
|
||||
--value "whsec_YOUR_SECRET"
|
||||
```
|
||||
|
||||
### 4.5 Update Function App Settings
|
||||
|
||||
```bash
|
||||
# Get secrets from Key Vault
|
||||
STRIPE_SECRET=$(az keyvault secret show \
|
||||
--vault-name YOUR_KEY_VAULT_NAME \
|
||||
--name "stripe-secret-key" \
|
||||
--query "value" -o tsv)
|
||||
|
||||
# Update Function App settings
|
||||
az functionapp config appsettings set \
|
||||
--name YOUR_FUNCTION_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--settings "[email protected](SecretUri=https://YOUR_KEY_VAULT_NAME.vault.azure.net/secrets/stripe-secret-key/)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Environment Variables
|
||||
|
||||
### 5.1 Create Environment File Template
|
||||
|
||||
Create `.env.production` file:
|
||||
|
||||
```bash
|
||||
# Azure Configuration
|
||||
AZURE_STATIC_WEB_APP_URL=https://miraclesinmotion.org
|
||||
AZURE_FUNCTION_APP_URL=https://YOUR_FUNCTION_APP.azurewebsites.net
|
||||
AZURE_CLIENT_ID=your-azure-client-id
|
||||
AZURE_TENANT_ID=your-azure-tenant-id
|
||||
|
||||
# Stripe Configuration
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_YOUR_KEY
|
||||
STRIPE_SECRET_KEY=sk_live_YOUR_KEY
|
||||
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET
|
||||
|
||||
# Cosmos DB Configuration
|
||||
COSMOS_DATABASE_NAME=MiraclesInMotion
|
||||
COSMOS_ENDPOINT=https://YOUR_COSMOS_ACCOUNT.documents.azure.com:443/
|
||||
|
||||
# Application Insights
|
||||
APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=YOUR_KEY
|
||||
|
||||
# Key Vault
|
||||
KEY_VAULT_URL=https://YOUR_KEY_VAULT_NAME.vault.azure.net/
|
||||
|
||||
# SignalR
|
||||
SIGNALR_CONNECTION_STRING=Endpoint=https://YOUR_SIGNALR.service.signalr.net;AccessKey=YOUR_KEY;
|
||||
|
||||
# Custom Domain
|
||||
CUSTOM_DOMAIN=miraclesinmotion.org
|
||||
```
|
||||
|
||||
### 5.2 Update Static Web App Configuration
|
||||
|
||||
```bash
|
||||
# Set environment variables for Static Web App
|
||||
az staticwebapp appsettings set \
|
||||
--name YOUR_STATIC_WEB_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod \
|
||||
--setting-names "VITE_STRIPE_PUBLISHABLE_KEY=pk_live_YOUR_KEY" \
|
||||
"AZURE_CLIENT_ID=your-azure-client-id" \
|
||||
"AZURE_TENANT_ID=your-azure-tenant-id"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Pre-Deployment Checklist
|
||||
|
||||
### 6.1 Azure Checklist
|
||||
|
||||
- [ ] Azure subscription created and active
|
||||
- [ ] Resource group created
|
||||
- [ ] Infrastructure deployed via Bicep
|
||||
- [ ] All Azure resources created successfully
|
||||
- [ ] Key Vault configured with secrets
|
||||
- [ ] Application Insights configured
|
||||
- [ ] Static Web App created
|
||||
- [ ] Function App created and configured
|
||||
- [ ] Cosmos DB database and containers created
|
||||
- [ ] RBAC permissions configured
|
||||
|
||||
### 6.2 MS Entra Checklist
|
||||
|
||||
- [ ] App registration created
|
||||
- [ ] Redirect URIs configured
|
||||
- [ ] API permissions granted
|
||||
- [ ] App roles created (Admin, Volunteer, Resource)
|
||||
- [ ] Users assigned to roles
|
||||
- [ ] Client ID and Tenant ID recorded
|
||||
- [ ] Client secret created (if needed)
|
||||
- [ ] Static Web App authentication configured
|
||||
|
||||
### 6.3 Cloudflare Checklist
|
||||
|
||||
- [ ] Domain added to Cloudflare
|
||||
- [ ] Nameservers updated at registrar
|
||||
- [ ] DNS records configured (CNAME for www and @)
|
||||
- [ ] SSL/TLS set to Full (strict)
|
||||
- [ ] Always Use HTTPS enabled
|
||||
- [ ] Page rules configured
|
||||
- [ ] Firewall rules configured
|
||||
- [ ] Security settings configured
|
||||
- [ ] Speed optimization enabled
|
||||
- [ ] Custom domain added to Azure Static Web App
|
||||
|
||||
### 6.4 Stripe Checklist
|
||||
|
||||
- [ ] Stripe account created and verified
|
||||
- [ ] API keys obtained (publishable and secret)
|
||||
- [ ] Webhook endpoint configured
|
||||
- [ ] Webhook signing secret obtained
|
||||
- [ ] Secrets stored in Key Vault
|
||||
- [ ] Function App configured with Stripe keys
|
||||
|
||||
### 6.5 Application Checklist
|
||||
|
||||
- [ ] Environment variables configured
|
||||
- [ ] staticwebapp.config.json updated
|
||||
- [ ] Authentication flow tested
|
||||
- [ ] API endpoints tested
|
||||
- [ ] Stripe integration tested
|
||||
- [ ] Monitoring configured
|
||||
- [ ] Logging configured
|
||||
|
||||
---
|
||||
|
||||
## 7. Post-Deployment Verification
|
||||
|
||||
### 7.1 Verify Azure Resources
|
||||
|
||||
```bash
|
||||
# Check Static Web App status
|
||||
az staticwebapp show \
|
||||
--name YOUR_STATIC_WEB_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod
|
||||
|
||||
# Check Function App status
|
||||
az functionapp show \
|
||||
--name YOUR_FUNCTION_APP_NAME \
|
||||
--resource-group rg-miraclesinmotion-prod
|
||||
|
||||
# Check Cosmos DB status
|
||||
az cosmosdb show \
|
||||
--name YOUR_COSMOS_ACCOUNT \
|
||||
--resource-group rg-miraclesinmotion-prod
|
||||
```
|
||||
|
||||
### 7.2 Verify Authentication
|
||||
|
||||
1. Navigate to `https://miraclesinmotion.org`
|
||||
2. Click "Sign In"
|
||||
3. Verify Microsoft authentication flow
|
||||
4. Verify user roles are assigned correctly
|
||||
5. Test role-based access control
|
||||
|
||||
### 7.3 Verify Cloudflare
|
||||
|
||||
```bash
|
||||
# Test DNS resolution
|
||||
dig miraclesinmotion.org
|
||||
dig www.miraclesinmotion.org
|
||||
|
||||
# Test HTTPS
|
||||
curl -I https://miraclesinmotion.org
|
||||
|
||||
# Verify Cloudflare headers
|
||||
curl -I https://miraclesinmotion.org | grep -i "cf-"
|
||||
```
|
||||
|
||||
### 7.4 Verify Stripe Integration
|
||||
|
||||
1. Test donation flow on the website
|
||||
2. Verify webhook events are received
|
||||
3. Check Stripe dashboard for transactions
|
||||
4. Verify payment processing
|
||||
|
||||
### 7.5 Verify Monitoring
|
||||
|
||||
1. Check Application Insights for telemetry
|
||||
2. Verify logs are being collected
|
||||
3. Set up alerts for critical issues
|
||||
4. Test error tracking
|
||||
|
||||
### 7.6 Performance Testing
|
||||
|
||||
```bash
|
||||
# Test page load times
|
||||
curl -w "@curl-format.txt" -o /dev/null -s https://miraclesinmotion.org
|
||||
|
||||
# Test API response times
|
||||
curl -w "@curl-format.txt" -o /dev/null -s https://miraclesinmotion.org/api/donations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
### 8.1 Common Issues
|
||||
|
||||
#### Authentication Not Working
|
||||
|
||||
- Verify app registration redirect URIs
|
||||
- Check Static Web App authentication configuration
|
||||
- Verify user roles are assigned
|
||||
- Check browser console for errors
|
||||
|
||||
#### DNS Not Resolving
|
||||
|
||||
- Verify nameservers are updated
|
||||
- Wait for DNS propagation (24-48 hours)
|
||||
- Check Cloudflare DNS records
|
||||
- Verify CNAME records point to correct Azure endpoint
|
||||
|
||||
#### SSL Certificate Issues
|
||||
|
||||
- Verify Cloudflare SSL mode is "Full (strict)"
|
||||
- Check Azure Static Web App custom domain configuration
|
||||
- Wait for SSL certificate provisioning (can take up to 24 hours)
|
||||
|
||||
#### Stripe Webhook Not Working
|
||||
|
||||
- Verify webhook endpoint URL is correct
|
||||
- Check webhook signing secret
|
||||
- Verify Function App is receiving webhook events
|
||||
- Check Function App logs for errors
|
||||
|
||||
### 8.2 Support Resources
|
||||
|
||||
- **Azure Documentation**: https://docs.microsoft.com/azure
|
||||
- **MS Entra Documentation**: https://docs.microsoft.com/azure/active-directory
|
||||
- **Cloudflare Documentation**: https://developers.cloudflare.com
|
||||
- **Stripe Documentation**: https://stripe.com/docs
|
||||
|
||||
---
|
||||
|
||||
## 9. Next Steps
|
||||
|
||||
After completing all prerequisites:
|
||||
|
||||
1. Deploy the application using the deployment script
|
||||
2. Verify all functionality
|
||||
3. Set up monitoring and alerts
|
||||
4. Configure backup and disaster recovery
|
||||
5. Set up CI/CD pipeline
|
||||
6. Schedule regular security audits
|
||||
7. Set up performance monitoring
|
||||
|
||||
---
|
||||
|
||||
## 10. Security Best Practices
|
||||
|
||||
1. **Never commit secrets to source control**
|
||||
2. **Use Key Vault for all secrets**
|
||||
3. **Enable MFA for all Azure accounts**
|
||||
4. **Regularly rotate API keys and secrets**
|
||||
5. **Monitor for suspicious activity**
|
||||
6. **Keep dependencies updated**
|
||||
7. **Use HTTPS everywhere**
|
||||
8. **Implement rate limiting**
|
||||
9. **Regular security audits**
|
||||
10. **Follow principle of least privilege**
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: January 2025
|
||||
**Maintained by**: Miracles In Motion Development Team
|
||||
## 6. Legacy Azure documentation
|
||||
|
||||
Historical docs under `docs/deployment/` and `docs/phases/` may still mention Azure. Treat them as **archived**; do not follow for new deploys.
|
||||
|
||||
+43
-108
@@ -1,139 +1,74 @@
|
||||
# Quick Start Guide
|
||||
|
||||
Fast path to get the Miracles in Motion project running, tested, and deployed.
|
||||
Fast path to run, test, and deploy the Miracles in Motion public site.
|
||||
|
||||
## 1. Prerequisites
|
||||
| Tool | Recommended Version | Notes |
|
||||
|------|---------------------|-------|
|
||||
| Node.js | 20.x / 22.x | Functions runtime Standard supports node:20; local dev can use 22 |
|
||||
| npm | 10+ | Bundled with recent Node |
|
||||
| Azure CLI | >= 2.60 | For infra & Static Web Apps commands |
|
||||
| SWA CLI (@azure/static-web-apps-cli) | latest | Local API + front-end emulation |
|
||||
| Git | latest | Source control |
|
||||
| WSL2 | Enabled | Shell environment (Ubuntu recommended) |
|
||||
|
||||
| Tool | Version | Notes |
|
||||
|------|---------|-------|
|
||||
| Node.js | 22.x | CI and local dev |
|
||||
| npm | 10+ | Use `npm ci` (`.npmrc` sets `legacy-peer-deps`) |
|
||||
| ImageMagick | any recent | Required for `npm run brand:export` / `prebuild` |
|
||||
| Git | latest | |
|
||||
|
||||
Production deploy additionally requires LAN access to Proxmox — see [PROXMOX_DEPLOYMENT.md](./deployment/PROXMOX_DEPLOYMENT.md).
|
||||
|
||||
## 2. Clone & install
|
||||
|
||||
```bash
|
||||
# Verify versions
|
||||
node -v
|
||||
npm -v
|
||||
az version
|
||||
git clone https://github.com/Order-of-Hospitallers/miracles_in_motion.git
|
||||
cd miracles_in_motion
|
||||
npm ci
|
||||
```
|
||||
|
||||
## 2. Clone & Install
|
||||
```bash
|
||||
git clone https://github.com/Miracles-In-Motion/public-web.git
|
||||
cd public-web
|
||||
npm install --legacy-peer-deps
|
||||
cd api && npm install --legacy-peer-deps && cd ..
|
||||
```
|
||||
## 3. Environment
|
||||
|
||||
## 3. Environment Setup
|
||||
Create a `.env.local` (frontend) and `api/local.settings.json` (Azure Functions) as needed.
|
||||
Copy `.env.example` to `.env.local` (do not commit secrets):
|
||||
|
||||
Example `.env.local` (do NOT commit secrets):
|
||||
```
|
||||
```env
|
||||
VITE_API_BASE=/api
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
|
||||
VITE_DEFAULT_LANGUAGE=en
|
||||
VITE_SUPPORTED_LANGUAGES=en,es,fr,de,zh,ar,pt,ru
|
||||
```
|
||||
|
||||
Example `api/local.settings.json`:
|
||||
```json
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "node"
|
||||
}
|
||||
}
|
||||
```
|
||||
## 4. Run locally
|
||||
|
||||
## 4. Run Locally (Integrated)
|
||||
Use SWA CLI to serve front-end + Functions together.
|
||||
```bash
|
||||
npm run build:api # Optional: compile API TypeScript
|
||||
swa start http://localhost:5173 --api-location ./api --devserver-run-command "npm run dev" --api-language node
|
||||
npm run dev
|
||||
# http://localhost:5173
|
||||
```
|
||||
If you prefer two terminals:
|
||||
|
||||
## 5. Test & validate
|
||||
|
||||
```bash
|
||||
npm run dev # Front-end (Vite)
|
||||
cd api && npm start # Functions runtime
|
||||
npm test # unit tests (Vitest)
|
||||
npm run type-check # tsc --noEmit
|
||||
npm run a11y:header-audit # WCAG header contrast tokens
|
||||
npm run validate:ci # all three above
|
||||
```
|
||||
|
||||
## 5. Testing
|
||||
## 6. Production build
|
||||
|
||||
```bash
|
||||
npm test # Front-end tests (Vitest / Testing Library)
|
||||
npm run build
|
||||
# output: dist/
|
||||
npm run preview # optional local preview
|
||||
```
|
||||
Add more tests under `src/components/__tests__/` or `src/test`.
|
||||
|
||||
## 6. Build
|
||||
## 7. Deploy to mim4u.org (Proxmox)
|
||||
|
||||
From the **proxmox** repo on operator LAN:
|
||||
|
||||
```bash
|
||||
npm run build # Produces front-end dist/
|
||||
cd api && npm run build # Compiles Functions to dist (if configured)
|
||||
./scripts/mim4u-deploy-to-7810.sh
|
||||
```
|
||||
|
||||
## 7. Azure Deployment (Static Web App Standard)
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
# Login
|
||||
az login
|
||||
|
||||
# Ensure resource group exists
|
||||
az group create --name rg-mim-prod --location eastus2
|
||||
|
||||
# Create Static Web App (front-end + managed functions)
|
||||
az staticwebapp create \
|
||||
--name mim-prod-web-standard \
|
||||
--resource-group rg-mim-prod \
|
||||
--location eastus2 \
|
||||
--source . \
|
||||
--branch main \
|
||||
--app-location / \
|
||||
--output-location dist
|
||||
curl -I -H 'Host: mim4u.org' http://192.168.11.37/
|
||||
```
|
||||
|
||||
To deploy updates without GitHub Actions (manual token):
|
||||
```bash
|
||||
TOKEN=$(az staticwebapp secrets list --name mim-prod-web-standard --resource-group rg-mim-prod --query properties.apiKey -o tsv)
|
||||
swa deploy ./dist --env production --deployment-token $TOKEN
|
||||
```
|
||||
## 8. CI
|
||||
|
||||
## 8. Custom Domain
|
||||
1. Add CNAME `www` → `<defaultHostname>`.
|
||||
2. Set hostname:
|
||||
```bash
|
||||
az staticwebapp hostname set \
|
||||
--name mim-prod-web-standard \
|
||||
--resource-group rg-mim-prod \
|
||||
--hostname miraclesinmotion.org
|
||||
```
|
||||
Azure provisions SSL automatically.
|
||||
|
||||
## 9. Configuration (staticwebapp.config.json)
|
||||
Key elements:
|
||||
- `navigationFallback` ensures SPA routing.
|
||||
- `globalHeaders` for security (CSP, HSTS). Adjust `Content-Security-Policy` as integrations evolve.
|
||||
|
||||
## 10. Useful Scripts
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `npm run dev` | Start Vite dev server |
|
||||
| `npm test` | Run tests |
|
||||
| `npm run build` | Build front-end |
|
||||
| `npm run analyze` | (If defined) Bundle analysis |
|
||||
|
||||
## 11. Troubleshooting
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| 404 on portal route | Ensure hash routing `/#/portals` or SPA fallback set |
|
||||
| Functions 500 error | Check `api` logs, run locally with `func start` if using standalone Functions |
|
||||
| CSP blocking script | Update CSP in `staticwebapp.config.json` to allow required domain |
|
||||
| Node version mismatch | Use Node 20 for SWA managed functions, 22 locally if desired |
|
||||
|
||||
## 12. Next Steps
|
||||
- Configure GitHub Actions for CI/CD.
|
||||
- Add monitoring (Application Insights) if using standalone Functions.
|
||||
- Replace test Stripe keys with live keys in production.
|
||||
|
||||
---
|
||||
Last updated: 2025-11-11
|
||||
GitHub Actions runs on PR (`validate.yml`) and main push (`deploy.yml`). See [DEPLOYMENT_PREREQUISITES.md](./DEPLOYMENT_PREREQUISITES.md).
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# MIM4U production deployment (Proxmox)
|
||||
|
||||
Production for **https://mim4u.org** is served from **VMID 7810** (mim-web-1 @ `192.168.11.37`) behind NPMplus. Azure Static Web Apps and Azure Functions are **not** used.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 22.x, npm 10+
|
||||
- ImageMagick (`convert`) for `npm run brand:export` (runs on `prebuild`)
|
||||
- LAN SSH to Proxmox host **192.168.11.12** (r630-02)
|
||||
- Sibling repo layout: `../proxmox` beside `miracles_in_motion`, or set `MIM_ROOT`
|
||||
|
||||
## Build and deploy
|
||||
|
||||
From the **proxmox** repo (operator LAN):
|
||||
|
||||
```bash
|
||||
./scripts/mim4u-deploy-to-7810.sh
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
cd ~/projects/miracles_in_motion
|
||||
npm run build
|
||||
# then tar dist/ into VMID 7810 /var/www/html (see deploy script)
|
||||
```
|
||||
|
||||
The deploy script **wipes** `/var/www/html/*` before extract to avoid stale PWA/JS bundles.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
curl -I -H 'Host: mim4u.org' http://192.168.11.37/
|
||||
curl -I -H 'Host: mim4u.org' http://192.168.11.37/brand/logo-horizontal-nav.svg
|
||||
```
|
||||
|
||||
Public HTTPS: NPMplus proxy hosts for `mim4u.org`, `www.mim4u.org`, `secure.mim4u.org`, `training.mim4u.org` → `192.168.11.37:80`.
|
||||
|
||||
## CI (GitHub Actions)
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `validate.yml` | Pull requests | type-check, tests, header WCAG audit, build |
|
||||
| `deploy.yml` | Push to `main` | validate + GitHub Pages artifact (optional mirror) |
|
||||
| `a11y-live.yml` | Weekly / manual | Lighthouse accessibility on live mim4u.org |
|
||||
|
||||
Canonical production traffic is **Proxmox 7810**, not GitHub Pages.
|
||||
|
||||
## API backend
|
||||
|
||||
`/api/*` on mim4u.org is proxied by nginx on **7810** to **VMID 7811** (mim-api-1 @ `192.168.11.36:3001`). API deployment is separate from this frontend repo.
|
||||
|
||||
## Related proxmox docs
|
||||
|
||||
- `docs/04-configuration/ALL_VMIDS_ENDPOINTS.md` — VMIDs and FQDNs
|
||||
- `docs/04-configuration/E2E_ENDPOINTS_LIST.md` — public routing verification
|
||||
+13
-39
@@ -1,46 +1,20 @@
|
||||
# 📚 Deployment Documentation
|
||||
# Deployment documentation
|
||||
|
||||
This directory contains all deployment-related documentation for the Miracles In Motion project.
|
||||
## Canonical (use this)
|
||||
|
||||
---
|
||||
| Doc | Purpose |
|
||||
|-----|---------|
|
||||
| [PROXMOX_DEPLOYMENT.md](./PROXMOX_DEPLOYMENT.md) | Production deploy to VMID 7810 |
|
||||
| [../DEPLOYMENT_PREREQUISITES.md](../DEPLOYMENT_PREREQUISITES.md) | Checklist and env vars |
|
||||
| [../QuickStart.md](../QuickStart.md) | Local dev, test, build |
|
||||
|
||||
## 📋 Documentation Files
|
||||
Proxmox operator script: `../../proxmox/scripts/mim4u-deploy-to-7810.sh` (sibling repo).
|
||||
|
||||
### Status & Reports
|
||||
- **DEPLOYMENT_STATUS.md** - Current deployment status and checklist
|
||||
- **DEPLOYMENT_STATUS_FINAL.md** - Final deployment status summary
|
||||
- **DEPLOYMENT_VERIFICATION_REPORT.md** - Deployment verification results
|
||||
- **DEPLOYMENT_COMPLETE.md** - Deployment completion summary
|
||||
## Archived (Azure — removed 2026-06)
|
||||
|
||||
### Guides & Instructions
|
||||
- **DEPLOYMENT_SETUP_README.md** - Deployment setup overview
|
||||
- **ALL_NEXT_STEPS.md** - Complete next steps for deployment
|
||||
- **COMPLETE_NEXT_STEPS.md** - Complete deployment steps guide
|
||||
- **DEPLOYMENT_COMPLETE_GUIDE.md** - Quick deployment guide
|
||||
- **FINAL_DEPLOYMENT_STEPS.md** - Final deployment steps
|
||||
The following files describe **deprecated** Azure Static Web Apps / Functions / Bicep flows. Do not use for new deployments:
|
||||
|
||||
### Next Steps & Tasks
|
||||
- **DEPLOYMENT_NEXT_STEPS.md** - Next steps for deployment
|
||||
- **NEXT_STEPS_COMPLETE.md** - Next steps completion summary
|
||||
- **REMAINING_TASKS_COMPLETE.md** - Remaining tasks summary
|
||||
|
||||
### Cloudflare & Domain
|
||||
- **CLOUDFLARE_SETUP.md** - Cloudflare setup instructions
|
||||
- **CLOUDFLARE_AUTOMATION_COMPLETE.md** - Cloudflare automation guide
|
||||
- **CUSTOM_DOMAIN_SETUP.md** - Custom domain configuration
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Check Current Status:** Start with `DEPLOYMENT_STATUS.md`
|
||||
2. **Follow Next Steps:** See `ALL_NEXT_STEPS.md` for complete guide
|
||||
3. **Cloudflare Setup:** See `CLOUDFLARE_SETUP.md` if using Cloudflare
|
||||
4. **Custom Domain:** See `CUSTOM_DOMAIN_SETUP.md` for domain configuration
|
||||
|
||||
---
|
||||
|
||||
## 📝 Note
|
||||
|
||||
All deployment documentation has been organized here from the project root for better structure and easier access.
|
||||
- `CLOUDFLARE_*.md`, `DEPLOYMENT_*.md`, `FINAL_*.md`, `ALL_NEXT_STEPS.md`, etc. in this folder
|
||||
- `docs/phases/*DEPLOYMENT*`, `PRODUCTION_DEPLOYMENT_SUCCESS.md`
|
||||
|
||||
Production is **https://mim4u.org** on Proxmox **7810** only.
|
||||
|
||||
+20
-56
@@ -1,65 +1,29 @@
|
||||
# Azure Configuration
|
||||
AZURE_STATIC_WEB_APP_URL=https://miraclesinmotion.org
|
||||
AZURE_FUNCTION_APP_URL=https://YOUR_FUNCTION_APP.azurewebsites.net
|
||||
AZURE_CLIENT_ID=your-azure-client-id
|
||||
AZURE_TENANT_ID=your-azure-tenant-id
|
||||
AZURE_CLIENT_SECRET=your-azure-client-secret
|
||||
# Production environment template (Proxmox / mim4u.org)
|
||||
# Copy relevant values to API host (VMID 7811) or operator .env — not all belong in Vite.
|
||||
|
||||
# Stripe Configuration
|
||||
# Public site
|
||||
CUSTOM_DOMAIN=mim4u.org
|
||||
VITE_API_BASE_URL=/api
|
||||
|
||||
# Stripe (public key in frontend; secret + webhook on API 7811)
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_YOUR_KEY
|
||||
STRIPE_SECRET_KEY=sk_live_YOUR_KEY
|
||||
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET
|
||||
|
||||
# Cosmos DB Configuration
|
||||
COSMOS_DATABASE_NAME=MiraclesInMotion
|
||||
COSMOS_ENDPOINT=https://YOUR_COSMOS_ACCOUNT.documents.azure.com:443/
|
||||
COSMOS_KEY=your-cosmos-key
|
||||
|
||||
# Application Insights
|
||||
APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=YOUR_KEY;IngestionEndpoint=https://YOUR_REGION.in.applicationinsights.azure.com/
|
||||
|
||||
# Key Vault
|
||||
KEY_VAULT_URL=https://YOUR_KEY_VAULT_NAME.vault.azure.net/
|
||||
|
||||
# SignalR
|
||||
SIGNALR_CONNECTION_STRING=Endpoint=https://YOUR_SIGNALR.service.signalr.net;AccessKey=YOUR_KEY;Version=1.0;
|
||||
|
||||
# Custom Domain
|
||||
CUSTOM_DOMAIN=miraclesinmotion.org
|
||||
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
VITE_API_BASE_URL=https://miraclesinmotion.org/api
|
||||
|
||||
# Feature Flags
|
||||
# Analytics
|
||||
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||
VITE_ENABLE_ANALYTICS=true
|
||||
|
||||
# Feature flags
|
||||
VITE_ENABLE_PWA=true
|
||||
VITE_ENABLE_AI=true
|
||||
VITE_ENABLE_CHAT=false
|
||||
NODE_ENV=production
|
||||
|
||||
# Cloudflare
|
||||
CLOUDFLARE_ZONE_ID=your-cloudflare-zone-id
|
||||
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
||||
|
||||
# Salesforce (Optional)
|
||||
SALESFORCE_CLIENT_ID=your-salesforce-client-id
|
||||
SALESFORCE_CLIENT_SECRET=your-salesforce-client-secret
|
||||
SALESFORCE_USERNAME=your-salesforce-username
|
||||
SALESFORCE_PASSWORD=your-salesforce-password
|
||||
SALESFORCE_SECURITY_TOKEN=your-salesforce-security-token
|
||||
|
||||
# Email Configuration (Optional)
|
||||
SMTP_HOST=smtp.office365.com
|
||||
SMTP_PORT=587
|
||||
[email protected]
|
||||
SMTP_PASSWORD=your-email-password
|
||||
[email protected]
|
||||
|
||||
# Monitoring
|
||||
SENTRY_DSN=your-sentry-dsn
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Security
|
||||
SESSION_SECRET=your-session-secret
|
||||
JWT_SECRET=your-jwt-secret
|
||||
ENCRYPTION_KEY=your-encryption-key
|
||||
# Cloudflare (DNS only — optional; use proxmox scripts)
|
||||
CLOUDFLARE_ZONE_ID=your-zone-id
|
||||
CLOUDFLARE_API_TOKEN=your-scoped-token
|
||||
|
||||
# Operator deploy (proxmox repo, not committed)
|
||||
# PROXMOX_HOST=192.168.11.12
|
||||
# VMID_MIM_WEB=7810
|
||||
# IP_MIM_WEB=192.168.11.37
|
||||
|
||||
@@ -1,472 +0,0 @@
|
||||
@description('Environment (dev, staging, prod)')
|
||||
param environment string = 'prod'
|
||||
|
||||
@description('Azure region for resources')
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('Stripe public key for payments')
|
||||
@secure()
|
||||
param stripePublicKey string
|
||||
|
||||
@description('Azure AD Client ID for authentication')
|
||||
param azureClientId string = ''
|
||||
|
||||
@description('Azure AD Tenant ID')
|
||||
param azureTenantId string = subscription().tenantId
|
||||
|
||||
@description('Azure AD Client Secret (optional, for server-side flows)')
|
||||
@secure()
|
||||
param azureClientSecret string = ''
|
||||
|
||||
@description('Custom domain name for the application')
|
||||
param customDomainName string = ''
|
||||
|
||||
@description('Enable custom domain configuration')
|
||||
param enableCustomDomain bool = false
|
||||
|
||||
@description('Static Web App SKU')
|
||||
@allowed(['Standard'])
|
||||
param staticWebAppSku string = 'Standard'
|
||||
|
||||
@description('Function App SKU (Y1 for Consumption, EP1/EP2/EP3 for Premium)')
|
||||
@allowed(['Y1', 'EP1', 'EP2', 'EP3'])
|
||||
param functionAppSku string = 'Y1'
|
||||
|
||||
// Variables
|
||||
var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 6)
|
||||
var resourcePrefix = 'mim-${environment}-${uniqueSuffix}'
|
||||
|
||||
// Log Analytics Workspace (needed first for Application Insights)
|
||||
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
|
||||
name: '${resourcePrefix}-logs'
|
||||
location: location
|
||||
properties: {
|
||||
sku: {
|
||||
name: 'PerGB2018'
|
||||
}
|
||||
retentionInDays: 30
|
||||
features: {
|
||||
searchVersion: 1
|
||||
legacy: 0
|
||||
enableLogAccessUsingOnlyResourcePermissions: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Application Insights
|
||||
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
||||
name: '${resourcePrefix}-appinsights'
|
||||
location: location
|
||||
kind: 'web'
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
Flow_Type: 'Redfield'
|
||||
Request_Source: 'IbizaAIExtension'
|
||||
RetentionInDays: 90
|
||||
WorkspaceResourceId: logAnalyticsWorkspace.id
|
||||
IngestionMode: 'LogAnalytics'
|
||||
publicNetworkAccessForIngestion: 'Enabled'
|
||||
publicNetworkAccessForQuery: 'Enabled'
|
||||
}
|
||||
}
|
||||
|
||||
// Key Vault
|
||||
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
|
||||
name: '${resourcePrefix}-kv'
|
||||
location: location
|
||||
properties: {
|
||||
sku: {
|
||||
family: 'A'
|
||||
name: 'standard'
|
||||
}
|
||||
tenantId: subscription().tenantId
|
||||
enableRbacAuthorization: true
|
||||
enableSoftDelete: true
|
||||
softDeleteRetentionInDays: 90
|
||||
enablePurgeProtection: true
|
||||
networkAcls: {
|
||||
defaultAction: 'Allow'
|
||||
bypass: 'AzureServices'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cosmos DB Account - Production Ready
|
||||
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = {
|
||||
name: '${resourcePrefix}-cosmos'
|
||||
location: location
|
||||
kind: 'GlobalDocumentDB'
|
||||
properties: {
|
||||
databaseAccountOfferType: 'Standard'
|
||||
consistencyPolicy: {
|
||||
defaultConsistencyLevel: 'Session'
|
||||
}
|
||||
locations: [
|
||||
{
|
||||
locationName: location
|
||||
failoverPriority: 0
|
||||
isZoneRedundant: true
|
||||
}
|
||||
]
|
||||
enableAutomaticFailover: true
|
||||
enableMultipleWriteLocations: false
|
||||
backupPolicy: {
|
||||
type: 'Periodic'
|
||||
periodicModeProperties: {
|
||||
backupIntervalInMinutes: 240
|
||||
backupRetentionIntervalInHours: 720
|
||||
backupStorageRedundancy: 'Geo'
|
||||
}
|
||||
}
|
||||
networkAclBypass: 'AzureServices'
|
||||
publicNetworkAccess: 'Enabled'
|
||||
}
|
||||
}
|
||||
|
||||
// Cosmos DB Database
|
||||
resource cosmosDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-05-15' = {
|
||||
parent: cosmosAccount
|
||||
name: 'MiraclesInMotion'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'MiraclesInMotion'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cosmos DB Containers
|
||||
resource donationsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
|
||||
parent: cosmosDatabase
|
||||
name: 'donations'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'donations'
|
||||
partitionKey: {
|
||||
paths: ['/id']
|
||||
kind: 'Hash'
|
||||
}
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent'
|
||||
automatic: true
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource volunteersContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
|
||||
parent: cosmosDatabase
|
||||
name: 'volunteers'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'volunteers'
|
||||
partitionKey: {
|
||||
paths: ['/id']
|
||||
kind: 'Hash'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource programsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
|
||||
parent: cosmosDatabase
|
||||
name: 'programs'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'programs'
|
||||
partitionKey: {
|
||||
paths: ['/id']
|
||||
kind: 'Hash'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource studentsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
|
||||
parent: cosmosDatabase
|
||||
name: 'students'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'students'
|
||||
partitionKey: {
|
||||
paths: ['/schoolId']
|
||||
kind: 'Hash'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function App Service Plan - Consumption Plan (Y1) for Production
|
||||
// Note: Changed from Premium to Consumption to avoid quota issues
|
||||
// Premium can be enabled later by requesting quota increase
|
||||
resource functionAppServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
|
||||
name: '${resourcePrefix}-func-plan'
|
||||
location: location
|
||||
sku: {
|
||||
name: functionAppSku
|
||||
tier: functionAppSku == 'Y1' ? 'Dynamic' : 'ElasticPremium'
|
||||
size: functionAppSku != 'Y1' ? functionAppSku : null
|
||||
capacity: functionAppSku != 'Y1' ? 1 : null
|
||||
}
|
||||
kind: 'functionapp'
|
||||
properties: {
|
||||
reserved: functionAppSku != 'Y1'
|
||||
maximumElasticWorkerCount: functionAppSku != 'Y1' ? 20 : null
|
||||
}
|
||||
}
|
||||
|
||||
// Storage Account for Function App
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
||||
name: replace('${resourcePrefix}stor', '-', '')
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Standard_LRS'
|
||||
}
|
||||
kind: 'StorageV2'
|
||||
properties: {
|
||||
supportsHttpsTrafficOnly: true
|
||||
encryption: {
|
||||
services: {
|
||||
file: {
|
||||
keyType: 'Account'
|
||||
enabled: true
|
||||
}
|
||||
blob: {
|
||||
keyType: 'Account'
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
keySource: 'Microsoft.Storage'
|
||||
}
|
||||
accessTier: 'Hot'
|
||||
}
|
||||
}
|
||||
|
||||
// Function App with Enhanced Configuration
|
||||
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
|
||||
name: '${resourcePrefix}-func'
|
||||
location: location
|
||||
kind: 'functionapp,linux'
|
||||
identity: {
|
||||
type: 'SystemAssigned'
|
||||
}
|
||||
properties: {
|
||||
serverFarmId: functionAppServicePlan.id
|
||||
siteConfig: {
|
||||
linuxFxVersion: 'NODE|22'
|
||||
appSettings: [
|
||||
{
|
||||
name: 'AzureWebJobsStorage'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${az.environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${az.environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTSHARE'
|
||||
value: toLower('${resourcePrefix}-func')
|
||||
}
|
||||
{
|
||||
name: 'FUNCTIONS_EXTENSION_VERSION'
|
||||
value: '~4'
|
||||
}
|
||||
{
|
||||
name: 'FUNCTIONS_WORKER_RUNTIME'
|
||||
value: 'node'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_NODE_DEFAULT_VERSION'
|
||||
value: '~22'
|
||||
}
|
||||
{
|
||||
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
|
||||
value: appInsights.properties.InstrumentationKey
|
||||
}
|
||||
{
|
||||
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
||||
value: appInsights.properties.ConnectionString
|
||||
}
|
||||
{
|
||||
name: 'COSMOS_CONNECTION_STRING'
|
||||
value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString
|
||||
}
|
||||
{
|
||||
name: 'COSMOS_DATABASE_NAME'
|
||||
value: 'MiraclesInMotion'
|
||||
}
|
||||
{
|
||||
name: 'KEY_VAULT_URL'
|
||||
value: keyVault.properties.vaultUri
|
||||
}
|
||||
{
|
||||
name: 'STRIPE_PUBLIC_KEY'
|
||||
value: stripePublicKey
|
||||
}
|
||||
]
|
||||
cors: {
|
||||
allowedOrigins: ['*']
|
||||
supportCredentials: false
|
||||
}
|
||||
use32BitWorkerProcess: false
|
||||
ftpsState: 'FtpsOnly'
|
||||
minTlsVersion: '1.2'
|
||||
}
|
||||
httpsOnly: true
|
||||
clientAffinityEnabled: false
|
||||
}
|
||||
}
|
||||
|
||||
// SignalR Service - Standard for Production
|
||||
resource signalR 'Microsoft.SignalRService/signalR@2023-02-01' = {
|
||||
name: '${resourcePrefix}-signalr'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Standard_S1'
|
||||
capacity: 1
|
||||
}
|
||||
kind: 'SignalR'
|
||||
properties: {
|
||||
features: [
|
||||
{
|
||||
flag: 'ServiceMode'
|
||||
value: 'Serverless'
|
||||
}
|
||||
]
|
||||
cors: {
|
||||
allowedOrigins: ['*']
|
||||
}
|
||||
networkACLs: {
|
||||
defaultAction: 'Allow'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Static Web App - Production Ready with Custom Domain Support
|
||||
resource staticWebApp 'Microsoft.Web/staticSites@2023-12-01' = {
|
||||
name: '${resourcePrefix}-web'
|
||||
location: 'Central US'
|
||||
sku: {
|
||||
name: staticWebAppSku
|
||||
tier: staticWebAppSku
|
||||
}
|
||||
properties: {
|
||||
buildProperties: {
|
||||
appLocation: '/'
|
||||
apiLocation: 'api'
|
||||
outputLocation: 'dist'
|
||||
}
|
||||
stagingEnvironmentPolicy: 'Enabled'
|
||||
allowConfigFileUpdates: true
|
||||
enterpriseGradeCdnStatus: 'Enabled'
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Static Web App authentication is configured via staticwebapp.config.json
|
||||
// and Azure Portal. App settings are configured separately through Azure Portal
|
||||
// or during deployment. The azureClientId and azureTenantId parameters are
|
||||
// stored in Key Vault for reference and can be used to configure authentication
|
||||
// in the Azure Portal after deployment.
|
||||
|
||||
// Custom Domain Configuration (if enabled)
|
||||
// Note: Using TXT validation for Enterprise Grade Edge compatibility
|
||||
resource customDomain 'Microsoft.Web/staticSites/customDomains@2023-12-01' = if (enableCustomDomain && !empty(customDomainName)) {
|
||||
parent: staticWebApp
|
||||
name: customDomainName
|
||||
properties: {
|
||||
validationMethod: 'txt-token'
|
||||
}
|
||||
}
|
||||
|
||||
// Key Vault Secrets
|
||||
resource cosmosConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
|
||||
parent: keyVault
|
||||
name: 'cosmos-connection-string'
|
||||
properties: {
|
||||
value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString
|
||||
}
|
||||
}
|
||||
|
||||
resource signalRConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
|
||||
parent: keyVault
|
||||
name: 'signalr-connection-string'
|
||||
properties: {
|
||||
value: signalR.listKeys().primaryConnectionString
|
||||
}
|
||||
}
|
||||
|
||||
resource stripeSecretKeySecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
|
||||
parent: keyVault
|
||||
name: 'stripe-secret-key'
|
||||
properties: {
|
||||
value: 'sk_live_placeholder' // Replace with actual secret key
|
||||
}
|
||||
}
|
||||
|
||||
// Azure AD Configuration Secrets
|
||||
resource azureClientIdSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = if (!empty(azureClientId)) {
|
||||
parent: keyVault
|
||||
name: 'azure-client-id'
|
||||
properties: {
|
||||
value: azureClientId
|
||||
}
|
||||
}
|
||||
|
||||
resource azureTenantIdSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
|
||||
parent: keyVault
|
||||
name: 'azure-tenant-id'
|
||||
properties: {
|
||||
value: azureTenantId
|
||||
}
|
||||
}
|
||||
|
||||
resource azureClientSecretSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = if (!empty(azureClientSecret)) {
|
||||
parent: keyVault
|
||||
name: 'azure-client-secret'
|
||||
properties: {
|
||||
value: azureClientSecret
|
||||
}
|
||||
}
|
||||
|
||||
// RBAC Assignments for Function App
|
||||
resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
||||
name: guid(keyVault.id, functionApp.id, 'Key Vault Secrets User')
|
||||
scope: keyVault
|
||||
properties: {
|
||||
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
|
||||
principalId: functionApp.identity.principalId
|
||||
principalType: 'ServicePrincipal'
|
||||
}
|
||||
}
|
||||
|
||||
resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
||||
name: guid(cosmosAccount.id, functionApp.id, 'Cosmos DB Built-in Data Contributor')
|
||||
scope: cosmosAccount
|
||||
properties: {
|
||||
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00000000-0000-0000-0000-000000000002') // Cosmos DB Built-in Data Contributor
|
||||
principalId: functionApp.identity.principalId
|
||||
principalType: 'ServicePrincipal'
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs
|
||||
output resourceGroupName string = resourceGroup().name
|
||||
output cosmosAccountName string = cosmosAccount.name
|
||||
output functionAppName string = functionApp.name
|
||||
output staticWebAppName string = staticWebApp.name
|
||||
output keyVaultName string = keyVault.name
|
||||
output appInsightsName string = appInsights.name
|
||||
output signalRName string = signalR.name
|
||||
output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.name
|
||||
output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
|
||||
output staticWebAppUrl string = 'https://${staticWebApp.properties.defaultHostname}'
|
||||
output customDomainName string = enableCustomDomain ? customDomainName : ''
|
||||
output applicationInsightsInstrumentationKey string = appInsights.properties.InstrumentationKey
|
||||
output applicationInsightsConnectionString string = appInsights.properties.ConnectionString
|
||||
output azureClientId string = azureClientId
|
||||
output azureTenantId string = azureTenantId
|
||||
output keyVaultUri string = keyVault.properties.vaultUri
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"environment": {
|
||||
"value": "prod"
|
||||
},
|
||||
"location": {
|
||||
"value": "East US"
|
||||
},
|
||||
"stripePublicKey": {
|
||||
"value": "pk_live_placeholder"
|
||||
},
|
||||
"azureClientId": {
|
||||
"value": ""
|
||||
},
|
||||
"azureTenantId": {
|
||||
"value": ""
|
||||
},
|
||||
"azureClientSecret": {
|
||||
"value": ""
|
||||
},
|
||||
"customDomainName": {
|
||||
"value": "mim4u.org"
|
||||
},
|
||||
"enableCustomDomain": {
|
||||
"value": true
|
||||
},
|
||||
"staticWebAppSku": {
|
||||
"value": "Standard"
|
||||
},
|
||||
"functionAppSku": {
|
||||
"value": "Y1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
@description('Environment (dev, staging, prod)')
|
||||
param environment string = 'prod'
|
||||
|
||||
@description('Azure region for resources')
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('Stripe public key for payments')
|
||||
@secure()
|
||||
param stripePublicKey string
|
||||
|
||||
// Variables
|
||||
var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 6)
|
||||
|
||||
// Cosmos DB Account
|
||||
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = {
|
||||
name: 'mim-${environment}-${uniqueSuffix}-cosmos'
|
||||
location: location
|
||||
kind: 'GlobalDocumentDB'
|
||||
properties: {
|
||||
databaseAccountOfferType: 'Standard'
|
||||
consistencyPolicy: {
|
||||
defaultConsistencyLevel: 'Session'
|
||||
}
|
||||
locations: [
|
||||
{
|
||||
locationName: location
|
||||
failoverPriority: 0
|
||||
isZoneRedundant: false
|
||||
}
|
||||
]
|
||||
capabilities: [
|
||||
{
|
||||
name: 'EnableServerless'
|
||||
}
|
||||
]
|
||||
backupPolicy: {
|
||||
type: 'Periodic'
|
||||
periodicModeProperties: {
|
||||
backupIntervalInMinutes: 240
|
||||
backupRetentionIntervalInHours: 720
|
||||
backupStorageRedundancy: 'Local'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cosmos DB Database
|
||||
resource cosmosDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-05-15' = {
|
||||
parent: cosmosAccount
|
||||
name: 'MiraclesInMotion'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'MiraclesInMotion'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cosmos DB Containers
|
||||
resource donationsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
|
||||
parent: cosmosDatabase
|
||||
name: 'donations'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'donations'
|
||||
partitionKey: {
|
||||
paths: ['/id']
|
||||
kind: 'Hash'
|
||||
}
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent'
|
||||
automatic: true
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource volunteersContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
|
||||
parent: cosmosDatabase
|
||||
name: 'volunteers'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'volunteers'
|
||||
partitionKey: {
|
||||
paths: ['/id']
|
||||
kind: 'Hash'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource programsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = {
|
||||
parent: cosmosDatabase
|
||||
name: 'programs'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'programs'
|
||||
partitionKey: {
|
||||
paths: ['/id']
|
||||
kind: 'Hash'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Key Vault
|
||||
resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' = {
|
||||
name: 'mim${environment}${uniqueSuffix}kv'
|
||||
location: location
|
||||
properties: {
|
||||
sku: {
|
||||
family: 'A'
|
||||
name: 'standard'
|
||||
}
|
||||
tenantId: tenant().tenantId
|
||||
accessPolicies: []
|
||||
enabledForDeployment: false
|
||||
enabledForDiskEncryption: false
|
||||
enabledForTemplateDeployment: true
|
||||
enableSoftDelete: true
|
||||
softDeleteRetentionInDays: 90
|
||||
enableRbacAuthorization: true
|
||||
}
|
||||
}
|
||||
|
||||
// Application Insights
|
||||
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
||||
name: 'mim-${environment}-${uniqueSuffix}-insights'
|
||||
location: location
|
||||
kind: 'web'
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
WorkspaceResourceId: logAnalyticsWorkspace.id
|
||||
}
|
||||
}
|
||||
|
||||
// Log Analytics Workspace
|
||||
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
||||
name: 'mim-${environment}-${uniqueSuffix}-logs'
|
||||
location: location
|
||||
properties: {
|
||||
sku: {
|
||||
name: 'PerGB2018'
|
||||
}
|
||||
retentionInDays: 30
|
||||
}
|
||||
}
|
||||
|
||||
// Storage Account for Functions
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
||||
name: 'mim${environment}${uniqueSuffix}st'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Standard_LRS'
|
||||
}
|
||||
kind: 'StorageV2'
|
||||
properties: {
|
||||
accessTier: 'Hot'
|
||||
supportsHttpsTrafficOnly: true
|
||||
minimumTlsVersion: 'TLS1_2'
|
||||
}
|
||||
}
|
||||
|
||||
// App Service Plan
|
||||
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
|
||||
name: 'mim-${environment}-${uniqueSuffix}-plan'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Y1'
|
||||
tier: 'Dynamic'
|
||||
}
|
||||
properties: {
|
||||
reserved: false
|
||||
}
|
||||
}
|
||||
|
||||
// Function App
|
||||
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
|
||||
name: 'mim-${environment}-${uniqueSuffix}-func'
|
||||
location: location
|
||||
kind: 'functionapp'
|
||||
identity: {
|
||||
type: 'SystemAssigned'
|
||||
}
|
||||
properties: {
|
||||
serverFarmId: appServicePlan.id
|
||||
siteConfig: {
|
||||
appSettings: [
|
||||
{
|
||||
name: 'AzureWebJobsStorage'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTSHARE'
|
||||
value: toLower('mim-${environment}-func')
|
||||
}
|
||||
{
|
||||
name: 'FUNCTIONS_EXTENSION_VERSION'
|
||||
value: '~4'
|
||||
}
|
||||
{
|
||||
name: 'FUNCTIONS_WORKER_RUNTIME'
|
||||
value: 'node'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_NODE_DEFAULT_VERSION'
|
||||
value: '~22'
|
||||
}
|
||||
{
|
||||
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
|
||||
value: appInsights.properties.InstrumentationKey
|
||||
}
|
||||
{
|
||||
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
||||
value: appInsights.properties.ConnectionString
|
||||
}
|
||||
{
|
||||
name: 'COSMOS_CONNECTION_STRING'
|
||||
value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString
|
||||
}
|
||||
{
|
||||
name: 'COSMOS_DATABASE_NAME'
|
||||
value: 'MiraclesInMotion'
|
||||
}
|
||||
{
|
||||
name: 'KEY_VAULT_URL'
|
||||
value: keyVault.properties.vaultUri
|
||||
}
|
||||
{
|
||||
name: 'STRIPE_PUBLIC_KEY'
|
||||
value: stripePublicKey
|
||||
}
|
||||
]
|
||||
}
|
||||
httpsOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
// SignalR Service
|
||||
resource signalR 'Microsoft.SignalRService/signalR@2023-02-01' = {
|
||||
name: 'mim-${environment}-${uniqueSuffix}-signalr'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Free_F1'
|
||||
capacity: 1
|
||||
}
|
||||
kind: 'SignalR'
|
||||
properties: {
|
||||
features: [
|
||||
{
|
||||
flag: 'ServiceMode'
|
||||
value: 'Serverless'
|
||||
}
|
||||
]
|
||||
cors: {
|
||||
allowedOrigins: ['*']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Static Web App
|
||||
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
|
||||
name: 'mim-${environment}-${uniqueSuffix}-web'
|
||||
location: 'Central US'
|
||||
sku: {
|
||||
name: 'Free'
|
||||
}
|
||||
properties: {
|
||||
buildProperties: {
|
||||
outputLocation: 'dist'
|
||||
apiLocation: ''
|
||||
appLocation: '/'
|
||||
}
|
||||
stagingEnvironmentPolicy: 'Enabled'
|
||||
}
|
||||
}
|
||||
|
||||
// Key Vault Secrets
|
||||
resource cosmosConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
|
||||
parent: keyVault
|
||||
name: 'cosmos-connection-string'
|
||||
properties: {
|
||||
value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString
|
||||
}
|
||||
}
|
||||
|
||||
resource signalRConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
|
||||
parent: keyVault
|
||||
name: 'signalr-connection-string'
|
||||
properties: {
|
||||
value: signalR.listKeys().primaryConnectionString
|
||||
}
|
||||
}
|
||||
|
||||
// RBAC Assignments for Function App
|
||||
resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
||||
name: guid(keyVault.id, functionApp.id, 'Key Vault Secrets User')
|
||||
scope: keyVault
|
||||
properties: {
|
||||
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
|
||||
principalId: functionApp.identity.principalId
|
||||
principalType: 'ServicePrincipal'
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs
|
||||
output resourceGroupName string = resourceGroup().name
|
||||
output cosmosAccountName string = cosmosAccount.name
|
||||
output functionAppName string = functionApp.name
|
||||
output staticWebAppName string = staticWebApp.name
|
||||
output keyVaultName string = keyVault.name
|
||||
output appInsightsName string = appInsights.name
|
||||
output signalRName string = signalR.name
|
||||
output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
|
||||
output staticWebAppUrl string = 'https://${staticWebApp.properties.defaultHostname}'
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"appName": {
|
||||
"value": "miraclesinmotion"
|
||||
},
|
||||
"environment": {
|
||||
"value": "prod"
|
||||
},
|
||||
"location": {
|
||||
"value": "eastus2"
|
||||
},
|
||||
"stripePublicKey": {
|
||||
"value": "pk_live_placeholder"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
# Production Deployment Script for Miracles in Motion
|
||||
# This script deploys the application to Azure with production SKUs and custom domain support
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ResourceGroupName = "rg-miraclesinmotion-prod",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Location = "East US",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CustomDomain = "mim4u.org",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$StripePublicKey = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$SkipBuild = $false
|
||||
)
|
||||
|
||||
Write-Host "🚀 Starting Production Deployment for Miracles in Motion" -ForegroundColor Green
|
||||
Write-Host "=================================================" -ForegroundColor Green
|
||||
|
||||
# Check if Azure CLI is installed
|
||||
if (!(Get-Command "az" -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "Azure CLI is not installed. Please install it first: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if Static Web Apps CLI is installed
|
||||
if (!(Get-Command "swa" -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "📦 Installing Azure Static Web Apps CLI..." -ForegroundColor Yellow
|
||||
npm install -g @azure/static-web-apps-cli
|
||||
}
|
||||
|
||||
# Login to Azure if not already logged in
|
||||
$currentAccount = az account show --query "user.name" -o tsv 2>$null
|
||||
if (!$currentAccount) {
|
||||
Write-Host "🔐 Please log in to Azure..." -ForegroundColor Yellow
|
||||
az login
|
||||
}
|
||||
|
||||
Write-Host "✅ Logged in as: $currentAccount" -ForegroundColor Green
|
||||
|
||||
# Create resource group if it doesn't exist
|
||||
Write-Host "📁 Creating resource group: $ResourceGroupName" -ForegroundColor Yellow
|
||||
az group create --name $ResourceGroupName --location $Location
|
||||
|
||||
# Validate Stripe key
|
||||
if ([string]::IsNullOrEmpty($StripePublicKey)) {
|
||||
$StripePublicKey = Read-Host "Enter your Stripe Public Key (pk_live_...)"
|
||||
}
|
||||
|
||||
if (!$StripePublicKey.StartsWith("pk_live_")) {
|
||||
Write-Warning "Warning: Using non-production Stripe key. For production, use pk_live_..."
|
||||
}
|
||||
|
||||
# Build and test the application
|
||||
if (!$SkipBuild) {
|
||||
Write-Host "🔨 Building the application..." -ForegroundColor Yellow
|
||||
|
||||
# Install dependencies
|
||||
Write-Host "📦 Installing main project dependencies..." -ForegroundColor Cyan
|
||||
npm install --legacy-peer-deps
|
||||
|
||||
# Install API dependencies
|
||||
Write-Host "📦 Installing API dependencies..." -ForegroundColor Cyan
|
||||
Set-Location api
|
||||
npm install
|
||||
Set-Location ..
|
||||
|
||||
# Run tests
|
||||
Write-Host "🧪 Running tests..." -ForegroundColor Cyan
|
||||
npx vitest run --reporter=verbose
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "Some tests failed, but continuing with deployment..."
|
||||
}
|
||||
|
||||
# Build the application
|
||||
Write-Host "🏗️ Building production bundle..." -ForegroundColor Cyan
|
||||
npm run build
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Build failed! Please fix the errors and try again."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Build completed successfully" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Deploy infrastructure
|
||||
Write-Host "🏗️ Deploying Azure infrastructure..." -ForegroundColor Yellow
|
||||
|
||||
$deploymentName = "mim-prod-deployment-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
||||
|
||||
$deploymentResult = az deployment group create `
|
||||
--resource-group $ResourceGroupName `
|
||||
--template-file "infrastructure/main-production.bicep" `
|
||||
--parameters "infrastructure/main-production.parameters.json" `
|
||||
--parameters stripePublicKey=$StripePublicKey `
|
||||
--parameters customDomainName=$CustomDomain `
|
||||
--parameters enableCustomDomain=$true `
|
||||
--name $deploymentName `
|
||||
--output json | ConvertFrom-Json
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Infrastructure deployment failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Infrastructure deployed successfully" -ForegroundColor Green
|
||||
|
||||
# Get deployment outputs
|
||||
$staticWebAppName = $deploymentResult.properties.outputs.staticWebAppName.value
|
||||
$functionAppName = $deploymentResult.properties.outputs.functionAppName.value
|
||||
$staticWebAppUrl = $deploymentResult.properties.outputs.staticWebAppUrl.value
|
||||
|
||||
Write-Host "📋 Deployment Details:" -ForegroundColor Cyan
|
||||
Write-Host " Static Web App: $staticWebAppName" -ForegroundColor White
|
||||
Write-Host " Function App: $functionAppName" -ForegroundColor White
|
||||
Write-Host " Primary URL: $staticWebAppUrl" -ForegroundColor White
|
||||
if ($CustomDomain) {
|
||||
Write-Host " Custom Domain: https://$CustomDomain" -ForegroundColor White
|
||||
}
|
||||
|
||||
# Get deployment token for Static Web App
|
||||
Write-Host "🔑 Getting deployment token..." -ForegroundColor Yellow
|
||||
$deploymentToken = az staticwebapp secrets list --name $staticWebAppName --resource-group $ResourceGroupName --query "properties.apiKey" -o tsv
|
||||
|
||||
if ([string]::IsNullOrEmpty($deploymentToken)) {
|
||||
Write-Error "Failed to get deployment token!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Deploy to Static Web App
|
||||
Write-Host "🚀 Deploying to Static Web App..." -ForegroundColor Yellow
|
||||
|
||||
$env:SWA_CLI_DEPLOYMENT_TOKEN = $deploymentToken
|
||||
|
||||
# Deploy using SWA CLI
|
||||
swa deploy ./dist --api-location ./api --env production --deployment-token $deploymentToken
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Static Web App deployment failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Application deployed successfully!" -ForegroundColor Green
|
||||
|
||||
# Deploy Function App
|
||||
Write-Host "🔧 Deploying Azure Functions..." -ForegroundColor Yellow
|
||||
|
||||
# Build API project
|
||||
Set-Location api
|
||||
npm run build 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Building API project..." -ForegroundColor Cyan
|
||||
npm run tsc 2>$null
|
||||
}
|
||||
Set-Location ..
|
||||
|
||||
# Deploy functions
|
||||
az functionapp deployment source config-zip --resource-group $ResourceGroupName --name $functionAppName --src "./api.zip" 2>$null
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ Azure Functions deployed successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Warning "Function deployment may have issues, but Static Web App is deployed"
|
||||
}
|
||||
|
||||
# Custom Domain Setup Instructions
|
||||
if ($CustomDomain) {
|
||||
Write-Host "🌐 Custom Domain Setup:" -ForegroundColor Magenta
|
||||
Write-Host "================================" -ForegroundColor Magenta
|
||||
Write-Host "1. Add a CNAME record in your DNS:" -ForegroundColor Yellow
|
||||
Write-Host " Name: www (or @)" -ForegroundColor White
|
||||
Write-Host " Value: $($staticWebAppUrl -replace 'https://', '')" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "2. Wait for DNS propagation (up to 48 hours)" -ForegroundColor Yellow
|
||||
Write-Host "3. The SSL certificate will be automatically provisioned" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Final Summary
|
||||
Write-Host "🎉 DEPLOYMENT COMPLETE!" -ForegroundColor Green
|
||||
Write-Host "========================" -ForegroundColor Green
|
||||
Write-Host "🌐 Primary URL: $staticWebAppUrl" -ForegroundColor Cyan
|
||||
if ($CustomDomain) {
|
||||
Write-Host "🌐 Custom Domain: https://$CustomDomain (after DNS setup)" -ForegroundColor Cyan
|
||||
}
|
||||
Write-Host "🔗 Portal Access: $staticWebAppUrl#/portals" -ForegroundColor Cyan
|
||||
Write-Host "📊 Analytics: $staticWebAppUrl#/analytics" -ForegroundColor Cyan
|
||||
Write-Host "🤖 AI Portal: $staticWebAppUrl#/ai-portal" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "📚 Next Steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. Set up DNS records for custom domain" -ForegroundColor White
|
||||
Write-Host "2. Configure authentication providers if needed" -ForegroundColor White
|
||||
Write-Host "3. Set up monitoring and alerts" -ForegroundColor White
|
||||
Write-Host "4. Update Stripe webhook endpoints" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "✨ Your Miracles in Motion application is now live in production!" -ForegroundColor Green
|
||||
@@ -1,62 +0,0 @@
|
||||
# Miracles in Motion - Production Deployment Script
|
||||
param(
|
||||
[string]$ResourceGroupName = "rg-miraclesinmotion-prod",
|
||||
[string]$Location = "East US 2",
|
||||
[string]$SubscriptionId = "6187c4d0-3c1a-4135-a8b5-c9782fcf0743"
|
||||
)
|
||||
|
||||
Write-Host "🚀 Starting Miracles in Motion Production Deployment" -ForegroundColor Green
|
||||
|
||||
# Set subscription
|
||||
Write-Host "Setting Azure subscription..." -ForegroundColor Yellow
|
||||
az account set --subscription $SubscriptionId
|
||||
|
||||
# Create resource group
|
||||
Write-Host "Creating resource group: $ResourceGroupName" -ForegroundColor Yellow
|
||||
az group create --name $ResourceGroupName --location $Location
|
||||
|
||||
# Deploy infrastructure using Bicep
|
||||
Write-Host "Deploying Azure infrastructure..." -ForegroundColor Yellow
|
||||
$deploymentResult = az deployment group create `
|
||||
--resource-group $ResourceGroupName `
|
||||
--template-file infrastructure/main.bicep `
|
||||
--parameters @infrastructure/main.parameters.json `
|
||||
--query 'properties.outputs' `
|
||||
--output json
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Infrastructure deployment failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Infrastructure deployed successfully!" -ForegroundColor Green
|
||||
|
||||
# Parse deployment outputs
|
||||
$outputs = $deploymentResult | ConvertFrom-Json
|
||||
|
||||
# Build and deploy Functions
|
||||
Write-Host "Building Azure Functions..." -ForegroundColor Yellow
|
||||
Set-Location api
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Deploy Functions
|
||||
Write-Host "Deploying Azure Functions..." -ForegroundColor Yellow
|
||||
func azure functionapp publish $outputs.functionAppName.value
|
||||
|
||||
# Build frontend
|
||||
Write-Host "Building frontend application..." -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Deploy to Static Web Apps
|
||||
Write-Host "Deploying to Azure Static Web Apps..." -ForegroundColor Yellow
|
||||
az staticwebapp deploy `
|
||||
--name $outputs.staticWebAppName.value `
|
||||
--resource-group $ResourceGroupName `
|
||||
--source dist/
|
||||
|
||||
Write-Host "🎉 Deployment completed successfully!" -ForegroundColor Green
|
||||
Write-Host "🌐 Frontend URL: https://$($outputs.staticWebAppName.value).azurestaticapps.net" -ForegroundColor Cyan
|
||||
Write-Host "⚡ Functions URL: https://$($outputs.functionAppName.value).azurewebsites.net" -ForegroundColor Cyan
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Simple Azure deployment script
|
||||
|
||||
Write-Host "🚀 Deploying Miracles in Motion to Azure..." -ForegroundColor Green
|
||||
|
||||
# Deploy infrastructure
|
||||
Write-Host "Deploying infrastructure..." -ForegroundColor Yellow
|
||||
$deployment = az deployment group create `
|
||||
--resource-group rg-miraclesinmotion-prod `
|
||||
--template-file infrastructure/main.bicep `
|
||||
--parameters @infrastructure/main.parameters.json `
|
||||
--name "infra-deploy-$(Get-Date -Format 'yyyyMMdd-HHmmss')" `
|
||||
--output json | ConvertFrom-Json
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "❌ Infrastructure deployment failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Infrastructure deployed successfully!" -ForegroundColor Green
|
||||
|
||||
# Get deployment outputs
|
||||
$functionAppName = $deployment.properties.outputs.functionAppName.value
|
||||
$staticWebAppName = $deployment.properties.outputs.staticWebAppName.value
|
||||
|
||||
Write-Host "Function App: $functionAppName" -ForegroundColor Cyan
|
||||
Write-Host "Static Web App: $staticWebAppName" -ForegroundColor Cyan
|
||||
|
||||
# Install Azure Functions Core Tools if needed
|
||||
Write-Host "Checking Azure Functions Core Tools..." -ForegroundColor Yellow
|
||||
try {
|
||||
func --version
|
||||
} catch {
|
||||
Write-Host "Installing Azure Functions Core Tools..." -ForegroundColor Yellow
|
||||
npm install -g azure-functions-core-tools@4 --unsafe-perm true
|
||||
}
|
||||
|
||||
# Deploy Functions
|
||||
Write-Host "Deploying Azure Functions..." -ForegroundColor Yellow
|
||||
Set-Location api
|
||||
func azure functionapp publish $functionAppName --typescript
|
||||
Set-Location ..
|
||||
|
||||
# Deploy Static Web App
|
||||
Write-Host "Deploying Static Web App..." -ForegroundColor Yellow
|
||||
az staticwebapp deploy `
|
||||
--name $staticWebAppName `
|
||||
--resource-group rg-miraclesinmotion-prod `
|
||||
--source dist/
|
||||
|
||||
Write-Host "🎉 Deployment completed successfully!" -ForegroundColor Green
|
||||
Write-Host "🌐 Frontend URL: https://$staticWebAppName.azurestaticapps.net" -ForegroundColor Cyan
|
||||
Write-Host "⚡ Functions URL: https://$functionAppName.azurewebsites.net" -ForegroundColor Cyan
|
||||
@@ -1,350 +0,0 @@
|
||||
# Deployment Checklist Script for Miracles In Motion
|
||||
# This script verifies all prerequisites are met before deployment
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ResourceGroupName = "rg-miraclesinmotion-prod",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$StaticWebAppName = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$FunctionAppName = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$SkipCloudflare = $false,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$SkipStripe = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Colors for output
|
||||
function Write-ColorOutput($ForegroundColor) {
|
||||
$fc = $host.UI.RawUI.ForegroundColor
|
||||
$host.UI.RawUI.ForegroundColor = $ForegroundColor
|
||||
if ($args) {
|
||||
Write-Output $args
|
||||
}
|
||||
$host.UI.RawUI.ForegroundColor = $fc
|
||||
}
|
||||
|
||||
Write-ColorOutput Green "🚀 Deployment Prerequisites Checklist"
|
||||
Write-Output "=========================================="
|
||||
Write-Output ""
|
||||
|
||||
$allChecksPassed = $true
|
||||
$checks = @()
|
||||
|
||||
# Function to add check result
|
||||
function Add-Check {
|
||||
param(
|
||||
[string]$Name,
|
||||
[bool]$Passed,
|
||||
[string]$Message = ""
|
||||
)
|
||||
|
||||
$checks += @{
|
||||
Name = $Name
|
||||
Passed = $Passed
|
||||
Message = $Message
|
||||
}
|
||||
|
||||
if (-not $Passed) {
|
||||
$script:allChecksPassed = $false
|
||||
}
|
||||
}
|
||||
|
||||
# 1. Azure CLI Check
|
||||
Write-ColorOutput Cyan "1. Checking Azure CLI..."
|
||||
try {
|
||||
$azVersion = az version --output json | ConvertFrom-Json
|
||||
Add-Check "Azure CLI" $true "Version: $($azVersion.'azure-cli')"
|
||||
Write-ColorOutput Green " ✅ Azure CLI installed"
|
||||
} catch {
|
||||
Add-Check "Azure CLI" $false "Azure CLI not found. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli"
|
||||
Write-ColorOutput Red " ❌ Azure CLI not found"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 2. Azure Login Check
|
||||
Write-ColorOutput Cyan "2. Checking Azure login status..."
|
||||
try {
|
||||
$account = az account show --output json 2>$null | ConvertFrom-Json
|
||||
if ($account) {
|
||||
Add-Check "Azure Login" $true "Logged in as: $($account.user.name)"
|
||||
Write-ColorOutput Green " ✅ Logged in to Azure"
|
||||
Write-Output " Subscription: $($account.name)"
|
||||
Write-Output " Tenant ID: $($account.tenantId)"
|
||||
} else {
|
||||
throw "Not logged in"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Azure Login" $false "Not logged in to Azure. Run: az login"
|
||||
Write-ColorOutput Red " ❌ Not logged in to Azure"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 3. Resource Group Check
|
||||
Write-ColorOutput Cyan "3. Checking resource group..."
|
||||
try {
|
||||
$rg = az group show --name $ResourceGroupName --output json 2>$null | ConvertFrom-Json
|
||||
if ($rg) {
|
||||
Add-Check "Resource Group" $true "Resource group exists: $($rg.name)"
|
||||
Write-ColorOutput Green " ✅ Resource group exists"
|
||||
Write-Output " Location: $($rg.location)"
|
||||
} else {
|
||||
throw "Resource group not found"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Resource Group" $false "Resource group not found: $ResourceGroupName"
|
||||
Write-ColorOutput Red " ❌ Resource group not found"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 4. Static Web App Check
|
||||
Write-ColorOutput Cyan "4. Checking Static Web App..."
|
||||
if ([string]::IsNullOrEmpty($StaticWebAppName)) {
|
||||
# Try to find Static Web App
|
||||
$swa = az staticwebapp list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
if ($swa) {
|
||||
$StaticWebAppName = $swa.name
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($StaticWebAppName)) {
|
||||
try {
|
||||
$swa = az staticwebapp show --name $StaticWebAppName --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json
|
||||
if ($swa) {
|
||||
Add-Check "Static Web App" $true "Static Web App exists: $($swa.name)"
|
||||
Write-ColorOutput Green " ✅ Static Web App exists"
|
||||
Write-Output " URL: https://$($swa.defaultHostname)"
|
||||
} else {
|
||||
throw "Static Web App not found"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Static Web App" $false "Static Web App not found: $StaticWebAppName"
|
||||
Write-ColorOutput Red " ❌ Static Web App not found"
|
||||
}
|
||||
} else {
|
||||
Add-Check "Static Web App" $false "Static Web App name not specified"
|
||||
Write-ColorOutput Red " ❌ Static Web App name not specified"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 5. Function App Check
|
||||
Write-ColorOutput Cyan "5. Checking Function App..."
|
||||
if ([string]::IsNullOrEmpty($FunctionAppName)) {
|
||||
# Try to find Function App
|
||||
$fa = az functionapp list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
if ($fa) {
|
||||
$FunctionAppName = $fa.name
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($FunctionAppName)) {
|
||||
try {
|
||||
$fa = az functionapp show --name $FunctionAppName --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json
|
||||
if ($fa) {
|
||||
Add-Check "Function App" $true "Function App exists: $($fa.name)"
|
||||
Write-ColorOutput Green " ✅ Function App exists"
|
||||
Write-Output " URL: https://$($fa.defaultHostName)"
|
||||
} else {
|
||||
throw "Function App not found"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Function App" $false "Function App not found: $FunctionAppName"
|
||||
Write-ColorOutput Red " ❌ Function App not found"
|
||||
}
|
||||
} else {
|
||||
Add-Check "Function App" $false "Function App name not specified"
|
||||
Write-ColorOutput Red " ❌ Function App name not specified"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 6. Key Vault Check
|
||||
Write-ColorOutput Cyan "6. Checking Key Vault..."
|
||||
try {
|
||||
$kv = az keyvault list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
if ($kv) {
|
||||
Add-Check "Key Vault" $true "Key Vault exists: $($kv.name)"
|
||||
Write-ColorOutput Green " ✅ Key Vault exists"
|
||||
|
||||
# Check for required secrets
|
||||
$requiredSecrets = @("stripe-secret-key", "azure-client-id", "azure-tenant-id")
|
||||
$missingSecrets = @()
|
||||
|
||||
foreach ($secret in $requiredSecrets) {
|
||||
try {
|
||||
$secretValue = az keyvault secret show --vault-name $kv.name --name $secret --output json 2>$null | ConvertFrom-Json
|
||||
if (-not $secretValue) {
|
||||
$missingSecrets += $secret
|
||||
}
|
||||
} catch {
|
||||
$missingSecrets += $secret
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingSecrets.Count -eq 0) {
|
||||
Write-ColorOutput Green " ✅ Required secrets present"
|
||||
} else {
|
||||
Write-ColorOutput Yellow " ⚠️ Missing secrets: $($missingSecrets -join ', ')"
|
||||
}
|
||||
} else {
|
||||
throw "Key Vault not found"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Key Vault" $false "Key Vault not found"
|
||||
Write-ColorOutput Red " ❌ Key Vault not found"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 7. Cosmos DB Check
|
||||
Write-ColorOutput Cyan "7. Checking Cosmos DB..."
|
||||
try {
|
||||
$cosmos = az cosmosdb list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
if ($cosmos) {
|
||||
Add-Check "Cosmos DB" $true "Cosmos DB exists: $($cosmos.name)"
|
||||
Write-ColorOutput Green " ✅ Cosmos DB exists"
|
||||
} else {
|
||||
throw "Cosmos DB not found"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Cosmos DB" $false "Cosmos DB not found"
|
||||
Write-ColorOutput Red " ❌ Cosmos DB not found"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 8. Application Insights Check
|
||||
Write-ColorOutput Cyan "8. Checking Application Insights..."
|
||||
try {
|
||||
$ai = az monitor app-insights component show --app $ResourceGroupName --output json 2>$null | ConvertFrom-Json
|
||||
if (-not $ai) {
|
||||
# Try alternative method
|
||||
$ai = az resource list --resource-group $ResourceGroupName --resource-type "Microsoft.Insights/components" --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
}
|
||||
if ($ai) {
|
||||
Add-Check "Application Insights" $true "Application Insights exists"
|
||||
Write-ColorOutput Green " ✅ Application Insights exists"
|
||||
} else {
|
||||
throw "Application Insights not found"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Application Insights" $false "Application Insights not found"
|
||||
Write-ColorOutput Red " ❌ Application Insights not found"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 9. Azure AD App Registration Check
|
||||
Write-ColorOutput Cyan "9. Checking Azure AD App Registration..."
|
||||
try {
|
||||
$appReg = az ad app list --display-name "Miracles In Motion Web App" --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
if ($appReg) {
|
||||
Add-Check "Azure AD App Registration" $true "App Registration exists: $($appReg.appId)"
|
||||
Write-ColorOutput Green " ✅ Azure AD App Registration exists"
|
||||
Write-Output " App ID: $($appReg.appId)"
|
||||
|
||||
# Check redirect URIs
|
||||
if ($appReg.web.redirectUris) {
|
||||
Write-Output " Redirect URIs: $($appReg.web.redirectUris.Count)"
|
||||
}
|
||||
} else {
|
||||
throw "App Registration not found"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Azure AD App Registration" $false "Azure AD App Registration not found"
|
||||
Write-ColorOutput Red " ❌ Azure AD App Registration not found"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# 10. Cloudflare Check
|
||||
if (-not $SkipCloudflare) {
|
||||
Write-ColorOutput Cyan "10. Checking Cloudflare configuration..."
|
||||
try {
|
||||
# Check DNS resolution
|
||||
$dnsResult = Resolve-DnsName -Name "miraclesinmotion.org" -ErrorAction SilentlyContinue
|
||||
if ($dnsResult) {
|
||||
Add-Check "Cloudflare DNS" $true "DNS resolution working"
|
||||
Write-ColorOutput Green " ✅ DNS resolution working"
|
||||
} else {
|
||||
Add-Check "Cloudflare DNS" $false "DNS resolution failed"
|
||||
Write-ColorOutput Red " ❌ DNS resolution failed"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Cloudflare DNS" $false "Could not verify DNS"
|
||||
Write-ColorOutput Yellow " ⚠️ Could not verify DNS"
|
||||
}
|
||||
Write-Output ""
|
||||
}
|
||||
|
||||
# 11. Stripe Check
|
||||
if (-not $SkipStripe) {
|
||||
Write-ColorOutput Cyan "11. Checking Stripe configuration..."
|
||||
try {
|
||||
if ($kv) {
|
||||
$stripeKey = az keyvault secret show --vault-name $kv.name --name "stripe-secret-key" --output json 2>$null | ConvertFrom-Json
|
||||
if ($stripeKey -and $stripeKey.value -like "sk_live_*") {
|
||||
Add-Check "Stripe Configuration" $true "Stripe keys configured"
|
||||
Write-ColorOutput Green " ✅ Stripe keys configured"
|
||||
} else {
|
||||
Add-Check "Stripe Configuration" $false "Stripe keys not configured or not production keys"
|
||||
Write-ColorOutput Yellow " ⚠️ Stripe keys not configured or not production keys"
|
||||
}
|
||||
} else {
|
||||
Add-Check "Stripe Configuration" $false "Key Vault not available"
|
||||
Write-ColorOutput Yellow " ⚠️ Key Vault not available"
|
||||
}
|
||||
} catch {
|
||||
Add-Check "Stripe Configuration" $false "Could not verify Stripe configuration"
|
||||
Write-ColorOutput Yellow " ⚠️ Could not verify Stripe configuration"
|
||||
}
|
||||
Write-Output ""
|
||||
}
|
||||
|
||||
# 12. Environment Variables Check
|
||||
Write-ColorOutput Cyan "12. Checking environment variables..."
|
||||
$envFile = ".env.production"
|
||||
if (Test-Path $envFile) {
|
||||
Add-Check "Environment File" $true "Environment file exists"
|
||||
Write-ColorOutput Green " ✅ Environment file exists"
|
||||
} else {
|
||||
Add-Check "Environment File" $false "Environment file not found: $envFile"
|
||||
Write-ColorOutput Yellow " ⚠️ Environment file not found"
|
||||
}
|
||||
Write-Output ""
|
||||
|
||||
# Summary
|
||||
Write-Output ""
|
||||
Write-ColorOutput Cyan "=========================================="
|
||||
Write-ColorOutput Cyan "Summary"
|
||||
Write-ColorOutput Cyan "=========================================="
|
||||
Write-Output ""
|
||||
|
||||
$passedChecks = ($checks | Where-Object { $_.Passed -eq $true }).Count
|
||||
$totalChecks = $checks.Count
|
||||
|
||||
Write-Output "Passed: $passedChecks / $totalChecks"
|
||||
Write-Output ""
|
||||
|
||||
foreach ($check in $checks) {
|
||||
if ($check.Passed) {
|
||||
Write-ColorOutput Green "✅ $($check.Name)"
|
||||
} else {
|
||||
Write-ColorOutput Red "❌ $($check.Name)"
|
||||
if ($check.Message) {
|
||||
Write-Output " $($check.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
|
||||
if ($allChecksPassed) {
|
||||
Write-ColorOutput Green "✅ All checks passed! Ready for deployment."
|
||||
exit 0
|
||||
} else {
|
||||
Write-ColorOutput Red "❌ Some checks failed. Please fix the issues before deploying."
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
# Script to populate .env file with Azure configuration
|
||||
# This script gathers Azure information and creates/updates the .env file
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ResourceGroupName = "rg-miraclesinmotion-prod",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Location = "eastus2",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Domain = "mim4u.org",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$CreateResourceGroup = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "🔧 Populating .env file with Azure configuration" -ForegroundColor Green
|
||||
Write-Host "=============================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check if logged in to Azure
|
||||
$account = az account show --output json 2>$null | ConvertFrom-Json
|
||||
if (-not $account) {
|
||||
Write-Host "❌ Not logged in to Azure. Please run: az login" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Logged in to Azure" -ForegroundColor Green
|
||||
Write-Host " Subscription: $($account.name)" -ForegroundColor Gray
|
||||
Write-Host " Tenant ID: $($account.tenantId)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# Get subscription ID
|
||||
$subscriptionId = $account.id
|
||||
$tenantId = $account.tenantId
|
||||
|
||||
# Check if resource group exists
|
||||
$rgExists = az group exists --name $ResourceGroupName --output tsv
|
||||
if ($rgExists -eq "false") {
|
||||
if ($CreateResourceGroup) {
|
||||
Write-Host "📁 Creating resource group: $ResourceGroupName" -ForegroundColor Cyan
|
||||
az group create --name $ResourceGroupName --location $Location | Out-Null
|
||||
Write-Host "✅ Resource group created" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Resource group '$ResourceGroupName' does not exist." -ForegroundColor Yellow
|
||||
Write-Host " Run with -CreateResourceGroup to create it, or deploy infrastructure first." -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "✅ Resource group exists: $ResourceGroupName" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Check for existing resources
|
||||
Write-Host "🔍 Checking for existing resources..." -ForegroundColor Cyan
|
||||
|
||||
# Check for Static Web App
|
||||
$staticWebApp = az staticwebapp list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
$staticWebAppName = ""
|
||||
$staticWebAppUrl = ""
|
||||
|
||||
if ($staticWebApp) {
|
||||
$staticWebAppName = $staticWebApp.name
|
||||
$staticWebAppUrl = "https://$($staticWebApp.defaultHostname)"
|
||||
Write-Host "✅ Found Static Web App: $staticWebAppName" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Static Web App not found (will use placeholder)" -ForegroundColor Yellow
|
||||
$staticWebAppUrl = "https://mim4u.org"
|
||||
}
|
||||
|
||||
# Check for Function App
|
||||
$functionApp = az functionapp list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
$functionAppName = ""
|
||||
$functionAppUrl = ""
|
||||
|
||||
if ($functionApp) {
|
||||
$functionAppName = $functionApp.name
|
||||
$functionAppUrl = "https://$($functionApp.defaultHostName)"
|
||||
Write-Host "✅ Found Function App: $functionAppName" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Function App not found (will use placeholder)" -ForegroundColor Yellow
|
||||
$functionAppUrl = "https://YOUR_FUNCTION_APP.azurewebsites.net"
|
||||
}
|
||||
|
||||
# Check for Key Vault
|
||||
$keyVault = az keyvault list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
$keyVaultName = ""
|
||||
$keyVaultUrl = ""
|
||||
|
||||
if ($keyVault) {
|
||||
$keyVaultName = $keyVault.name
|
||||
$keyVaultUrl = "https://$keyVaultName.vault.azure.net/"
|
||||
Write-Host "✅ Found Key Vault: $keyVaultName" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Key Vault not found (will use placeholder)" -ForegroundColor Yellow
|
||||
$keyVaultUrl = "https://YOUR_KEY_VAULT_NAME.vault.azure.net/"
|
||||
}
|
||||
|
||||
# Check for Cosmos DB
|
||||
$cosmosAccount = az cosmosdb list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
$cosmosEndpoint = ""
|
||||
|
||||
if ($cosmosAccount) {
|
||||
$cosmosEndpoint = "https://$($cosmosAccount.name).documents.azure.com:443/"
|
||||
Write-Host "✅ Found Cosmos DB: $($cosmosAccount.name)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Cosmos DB not found (will use placeholder)" -ForegroundColor Yellow
|
||||
$cosmosEndpoint = "https://YOUR_COSMOS_ACCOUNT.documents.azure.com:443/"
|
||||
}
|
||||
|
||||
# Check for Application Insights
|
||||
$appInsights = az monitor app-insights component show --app $ResourceGroupName --output json 2>$null | ConvertFrom-Json
|
||||
if (-not $appInsights) {
|
||||
$appInsights = az resource list --resource-group $ResourceGroupName --resource-type "Microsoft.Insights/components" --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
}
|
||||
$appInsightsConnectionString = ""
|
||||
|
||||
if ($appInsights) {
|
||||
$appInsightsConnectionString = $appInsights.connectionString
|
||||
Write-Host "✅ Found Application Insights: $($appInsights.name)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Application Insights not found (will use placeholder)" -ForegroundColor Yellow
|
||||
$appInsightsConnectionString = "InstrumentationKey=YOUR_KEY;IngestionEndpoint=https://YOUR_REGION.in.applicationinsights.azure.com/"
|
||||
}
|
||||
|
||||
# Check for SignalR
|
||||
$signalR = az signalr list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
$signalRConnectionString = ""
|
||||
|
||||
if ($signalR) {
|
||||
$signalRKeys = az signalr key list --name $signalR.name --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json
|
||||
if ($signalRKeys) {
|
||||
$signalREndpoint = $signalR.hostName
|
||||
$signalRKey = $signalRKeys.primaryKey
|
||||
$signalRConnectionString = "Endpoint=https://$signalREndpoint;AccessKey=$signalRKey;Version=1.0;"
|
||||
Write-Host "✅ Found SignalR: $($signalR.name)" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠️ SignalR not found (will use placeholder)" -ForegroundColor Yellow
|
||||
$signalRConnectionString = "Endpoint=https://YOUR_SIGNALR.service.signalr.net;AccessKey=YOUR_KEY;Version=1.0;"
|
||||
}
|
||||
|
||||
# Check for Azure AD App Registration
|
||||
$appReg = az ad app list --display-name "Miracles In Motion Web App" --output json 2>$null | ConvertFrom-Json | Select-Object -First 1
|
||||
$azureClientId = ""
|
||||
|
||||
if ($appReg) {
|
||||
$azureClientId = $appReg.appId
|
||||
Write-Host "✅ Found Azure AD App Registration: $azureClientId" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Azure AD App Registration not found (will use placeholder)" -ForegroundColor Yellow
|
||||
Write-Host " Run: .\scripts\setup-azure-entra.ps1 to create it" -ForegroundColor Yellow
|
||||
$azureClientId = "your-azure-client-id"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Prompt for Stripe keys
|
||||
Write-Host "💳 Stripe Configuration" -ForegroundColor Cyan
|
||||
$stripePublishableKey = Read-Host "Enter Stripe Publishable Key (pk_live_...) [or press Enter to skip]"
|
||||
if ([string]::IsNullOrWhiteSpace($stripePublishableKey)) {
|
||||
$stripePublishableKey = "pk_live_YOUR_KEY"
|
||||
}
|
||||
|
||||
$stripeSecretKey = Read-Host "Enter Stripe Secret Key (sk_live_...) [or press Enter to skip]"
|
||||
if ([string]::IsNullOrWhiteSpace($stripeSecretKey)) {
|
||||
$stripeSecretKey = "sk_live_YOUR_KEY"
|
||||
}
|
||||
|
||||
$stripeWebhookSecret = Read-Host "Enter Stripe Webhook Secret (whsec_...) [or press Enter to skip]"
|
||||
if ([string]::IsNullOrWhiteSpace($stripeWebhookSecret)) {
|
||||
$stripeWebhookSecret = "whsec_YOUR_SECRET"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Create .env file content
|
||||
$envContent = @"
|
||||
# Azure Configuration
|
||||
AZURE_SUBSCRIPTION_ID=$subscriptionId
|
||||
AZURE_TENANT_ID=$tenantId
|
||||
AZURE_RESOURCE_GROUP=$ResourceGroupName
|
||||
AZURE_LOCATION=$Location
|
||||
AZURE_STATIC_WEB_APP_URL=$staticWebAppUrl
|
||||
AZURE_STATIC_WEB_APP_NAME=$staticWebAppName
|
||||
AZURE_FUNCTION_APP_URL=$functionAppUrl
|
||||
AZURE_FUNCTION_APP_NAME=$functionAppName
|
||||
AZURE_CLIENT_ID=$azureClientId
|
||||
AZURE_TENANT_ID=$tenantId
|
||||
AZURE_CLIENT_SECRET=your-azure-client-secret
|
||||
|
||||
# Stripe Configuration
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=$stripePublishableKey
|
||||
STRIPE_SECRET_KEY=$stripeSecretKey
|
||||
STRIPE_WEBHOOK_SECRET=$stripeWebhookSecret
|
||||
|
||||
# Cosmos DB Configuration
|
||||
COSMOS_DATABASE_NAME=MiraclesInMotion
|
||||
COSMOS_ENDPOINT=$cosmosEndpoint
|
||||
COSMOS_KEY=your-cosmos-key
|
||||
|
||||
# Application Insights
|
||||
APPLICATIONINSIGHTS_CONNECTION_STRING=$appInsightsConnectionString
|
||||
|
||||
# Key Vault
|
||||
KEY_VAULT_URL=$keyVaultUrl
|
||||
KEY_VAULT_NAME=$keyVaultName
|
||||
|
||||
# SignalR
|
||||
SIGNALR_CONNECTION_STRING=$signalRConnectionString
|
||||
|
||||
# Custom Domain
|
||||
CUSTOM_DOMAIN=$Domain
|
||||
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
VITE_API_BASE_URL=$staticWebAppUrl/api
|
||||
|
||||
# Feature Flags
|
||||
VITE_ENABLE_ANALYTICS=true
|
||||
VITE_ENABLE_PWA=true
|
||||
VITE_ENABLE_AI=true
|
||||
|
||||
# Cloudflare (Optional)
|
||||
CLOUDFLARE_ZONE_ID=your-cloudflare-zone-id
|
||||
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
||||
|
||||
# Salesforce (Optional)
|
||||
SALESFORCE_CLIENT_ID=your-salesforce-client-id
|
||||
SALESFORCE_CLIENT_SECRET=your-salesforce-client-secret
|
||||
SALESFORCE_USERNAME=your-salesforce-username
|
||||
SALESFORCE_PASSWORD=your-salesforce-password
|
||||
SALESFORCE_SECURITY_TOKEN=your-salesforce-security-token
|
||||
|
||||
# Email Configuration (Optional)
|
||||
SMTP_HOST=smtp.office365.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@domain.com
|
||||
SMTP_PASSWORD=your-email-password
|
||||
SMTP_FROM=noreply@mim4u.org
|
||||
|
||||
# Monitoring (Optional)
|
||||
SENTRY_DSN=your-sentry-dsn
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Security
|
||||
SESSION_SECRET=your-session-secret
|
||||
JWT_SECRET=your-jwt-secret
|
||||
ENCRYPTION_KEY=your-encryption-key
|
||||
"@
|
||||
|
||||
# Write .env file
|
||||
$envFile = ".env.production"
|
||||
$envContent | Out-File -FilePath $envFile -Encoding UTF8 -NoNewline
|
||||
|
||||
Write-Host "✅ Created .env file: $envFile" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "📋 Summary:" -ForegroundColor Cyan
|
||||
Write-Host " Subscription: $($account.name)" -ForegroundColor Gray
|
||||
Write-Host " Tenant ID: $tenantId" -ForegroundColor Gray
|
||||
Write-Host " Resource Group: $ResourceGroupName" -ForegroundColor Gray
|
||||
Write-Host " Domain: $Domain" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "⚠️ Next Steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. Review and update placeholder values in $envFile" -ForegroundColor White
|
||||
Write-Host "2. Run: .\scripts\setup-azure-entra.ps1 to create Azure AD app registration" -ForegroundColor White
|
||||
Write-Host "3. Deploy infrastructure: az deployment group create ..." -ForegroundColor White
|
||||
Write-Host "4. Store secrets in Key Vault using: .\scripts\store-secrets-in-keyvault.ps1" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
# MS Entra (Azure AD) Setup Script for Miracles In Motion (PowerShell)
|
||||
# This script helps configure Azure AD authentication for the application
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$AppName = "Miracles In Motion Web App",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Domain = "mim4u.org",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$StaticWebAppName = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$AzureResourceGroup = "rg-miraclesinmotion-prod",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$KeyVaultName = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "🔐 MS Entra (Azure AD) Setup Script" -ForegroundColor Green
|
||||
Write-Host "==========================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check if Azure CLI is installed
|
||||
if (-not (Get-Command "az" -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "❌ Azure CLI not found. Please install it first." -ForegroundColor Red
|
||||
Write-Host "Install from: https://docs.microsoft.com/cli/azure/install-azure-cli" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if logged in to Azure
|
||||
Write-Host "📋 Checking Azure login status..." -ForegroundColor Cyan
|
||||
$account = az account show --output json 2>$null | ConvertFrom-Json
|
||||
if (-not $account) {
|
||||
Write-Host "⚠️ Not logged in to Azure. Please log in..." -ForegroundColor Yellow
|
||||
az login
|
||||
$account = az account show --output json | ConvertFrom-Json
|
||||
}
|
||||
Write-Host "✅ Logged in as: $($account.user.name)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Get Azure Static Web App URL
|
||||
Write-Host "📋 Getting Azure Static Web App information..." -ForegroundColor Cyan
|
||||
|
||||
if ([string]::IsNullOrEmpty($StaticWebAppName)) {
|
||||
# Try to find Static Web App
|
||||
$swa = az staticwebapp list --resource-group $AzureResourceGroup --output json | ConvertFrom-Json | Select-Object -First 1
|
||||
if ($swa) {
|
||||
$StaticWebAppName = $swa.name
|
||||
}
|
||||
}
|
||||
|
||||
$azureStaticWebAppUrl = ""
|
||||
if (-not [string]::IsNullOrEmpty($StaticWebAppName)) {
|
||||
$azureStaticWebAppUrl = az staticwebapp show `
|
||||
--name $StaticWebAppName `
|
||||
--resource-group $AzureResourceGroup `
|
||||
--query "defaultHostname" -o tsv 2>$null
|
||||
|
||||
if ($azureStaticWebAppUrl) {
|
||||
$azureStaticWebAppUrl = "https://$azureStaticWebAppUrl"
|
||||
Write-Host "✅ Static Web App URL: $azureStaticWebAppUrl" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠️ Static Web App not found. Using default URL format." -ForegroundColor Yellow
|
||||
$azureStaticWebAppUrl = "https://${StaticWebAppName}.azurestaticapps.net"
|
||||
}
|
||||
|
||||
$productionUrl = "https://$Domain"
|
||||
$wwwUrl = "https://www.$Domain"
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Get Tenant ID
|
||||
$tenantId = $account.tenantId
|
||||
Write-Host "✅ Tenant ID: $tenantId" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check if app registration already exists
|
||||
Write-Host "🔍 Checking for existing app registration..." -ForegroundColor Cyan
|
||||
$existingApp = az ad app list --display-name $AppName --output json | ConvertFrom-Json | Select-Object -First 1
|
||||
|
||||
if ($existingApp) {
|
||||
Write-Host "⚠️ App registration already exists: $($existingApp.appId)" -ForegroundColor Yellow
|
||||
$updateApp = Read-Host "Do you want to update it? (y/n)"
|
||||
if ($updateApp -ne "y") {
|
||||
$appId = $existingApp.appId
|
||||
Write-Host "✅ Using existing app registration" -ForegroundColor Green
|
||||
} else {
|
||||
$appId = $existingApp.appId
|
||||
Write-Host "📝 Updating app registration..." -ForegroundColor Cyan
|
||||
}
|
||||
} else {
|
||||
# Create app registration
|
||||
Write-Host "📝 Creating app registration..." -ForegroundColor Cyan
|
||||
$appId = az ad app create `
|
||||
--display-name $AppName `
|
||||
--sign-in-audience "AzureADMultipleOrgs" `
|
||||
--web-redirect-uris $productionUrl $wwwUrl $azureStaticWebAppUrl `
|
||||
--query "appId" -o tsv
|
||||
|
||||
Write-Host "✅ App registration created: $appId" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Update redirect URIs
|
||||
Write-Host "📝 Updating redirect URIs..." -ForegroundColor Cyan
|
||||
az ad app update --id $appId `
|
||||
--web-redirect-uris $productionUrl $wwwUrl $azureStaticWebAppUrl `
|
||||
--enable-id-token-issuance true `
|
||||
--enable-access-token-issuance false | Out-Null
|
||||
|
||||
Write-Host "✅ Redirect URIs updated" -ForegroundColor Green
|
||||
Write-Host " - $productionUrl"
|
||||
Write-Host " - $wwwUrl"
|
||||
Write-Host " - $azureStaticWebAppUrl"
|
||||
Write-Host ""
|
||||
|
||||
# Configure API permissions
|
||||
Write-Host "📝 Configuring API permissions..." -ForegroundColor Cyan
|
||||
|
||||
$graphPermissions = @(
|
||||
"User.Read",
|
||||
"User.ReadBasic.All",
|
||||
"email",
|
||||
"openid",
|
||||
"profile"
|
||||
)
|
||||
|
||||
foreach ($permission in $graphPermissions) {
|
||||
Write-Host " Adding permission: $permission" -ForegroundColor Gray
|
||||
$permissionId = az ad sp show --id "00000003-0000-0000-c000-000000000000" --query "oauth2PermissionScopes[?value=='$permission'].id" -o tsv
|
||||
if ($permissionId) {
|
||||
az ad app permission add `
|
||||
--id $appId `
|
||||
--api "00000003-0000-0000-c000-000000000000" `
|
||||
--api-permissions "${permissionId}=Scope" 2>$null | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "✅ API permissions configured" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Create service principal
|
||||
Write-Host "📝 Creating service principal..." -ForegroundColor Cyan
|
||||
$spId = az ad sp create --id $appId --query "id" -o tsv 2>$null
|
||||
if (-not $spId) {
|
||||
$spId = az ad sp show --id $appId --query "id" -o tsv
|
||||
}
|
||||
|
||||
Write-Host "✅ Service principal created: $spId" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Grant admin consent
|
||||
Write-Host "📝 Granting admin consent for API permissions..." -ForegroundColor Cyan
|
||||
$hasAdmin = Read-Host "Do you have admin privileges to grant consent? (y/n)"
|
||||
|
||||
if ($hasAdmin -eq "y") {
|
||||
az ad app permission admin-consent --id $appId 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ Admin consent granted" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Could not grant admin consent. You may need to do this manually." -ForegroundColor Yellow
|
||||
Write-Host " Go to: Azure Portal → Microsoft Entra ID → App registrations → $AppName → API permissions → Grant admin consent" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠️ Skipping admin consent. Please grant consent manually in Azure Portal." -ForegroundColor Yellow
|
||||
Write-Host " Go to: Azure Portal → Microsoft Entra ID → App registrations → $AppName → API permissions → Grant admin consent" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Create client secret
|
||||
Write-Host "📝 Client Secret Configuration..." -ForegroundColor Cyan
|
||||
$createSecret = Read-Host "Do you want to create a client secret? (y/n)"
|
||||
|
||||
$clientSecret = ""
|
||||
if ($createSecret -eq "y") {
|
||||
$secretName = "Miracles In Motion Secret $(Get-Date -Format 'yyyyMMdd')"
|
||||
$clientSecret = az ad app credential reset --id $appId --display-name $secretName --years 2 --query "password" -o tsv
|
||||
Write-Host "✅ Client secret created" -ForegroundColor Green
|
||||
Write-Host "⚠️ IMPORTANT: Save this secret now - it won't be shown again!" -ForegroundColor Red
|
||||
Write-Host "Secret: $clientSecret" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Read-Host "Press Enter to continue after saving the secret..."
|
||||
} else {
|
||||
Write-Host "⚠️ Skipping client secret creation" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Store configuration in Key Vault
|
||||
Write-Host "📝 Storing configuration in Key Vault..." -ForegroundColor Cyan
|
||||
|
||||
if ([string]::IsNullOrEmpty($KeyVaultName)) {
|
||||
$KeyVaultName = az keyvault list --resource-group $AzureResourceGroup --query "[0].name" -o tsv 2>$null
|
||||
}
|
||||
|
||||
if ($KeyVaultName) {
|
||||
Write-Host "Storing in Key Vault: $KeyVaultName" -ForegroundColor Gray
|
||||
|
||||
az keyvault secret set --vault-name $KeyVaultName --name "azure-client-id" --value $appId 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ Client ID stored" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Could not store Client ID" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
az keyvault secret set --vault-name $KeyVaultName --name "azure-tenant-id" --value $tenantId 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ Tenant ID stored" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Could not store Tenant ID" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($clientSecret) {
|
||||
az keyvault secret set --vault-name $KeyVaultName --name "azure-client-secret" --value $clientSecret 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ Client Secret stored" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ Could not store Client Secret" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠️ Key Vault not found. Skipping secret storage." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Summary
|
||||
Write-Host "✅ MS Entra Setup Complete!" -ForegroundColor Green
|
||||
Write-Host "==================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Configuration Summary:"
|
||||
Write-Host " App Registration ID: $appId"
|
||||
Write-Host " Tenant ID: $tenantId"
|
||||
Write-Host " Service Principal ID: $spId"
|
||||
Write-Host ""
|
||||
Write-Host "Redirect URIs:"
|
||||
Write-Host " - $productionUrl"
|
||||
Write-Host " - $wwwUrl"
|
||||
Write-Host " - $azureStaticWebAppUrl"
|
||||
Write-Host ""
|
||||
Write-Host "Next Steps:"
|
||||
Write-Host "1. Assign users to app roles in Azure Portal"
|
||||
Write-Host "2. Update staticwebapp.config.json with authentication configuration"
|
||||
Write-Host "3. Update application code to use Azure AD authentication"
|
||||
Write-Host "4. Test authentication flow"
|
||||
Write-Host ""
|
||||
Write-Host "Azure Portal Links:"
|
||||
Write-Host " App Registration: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$appId"
|
||||
Write-Host " API Permissions: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$appId"
|
||||
Write-Host ""
|
||||
|
||||
# Export variables
|
||||
$configContent = @"
|
||||
# Azure AD Configuration
|
||||
AZURE_CLIENT_ID=$appId
|
||||
AZURE_TENANT_ID=$tenantId
|
||||
AZURE_CLIENT_SECRET=$clientSecret
|
||||
AZURE_STATIC_WEB_APP_URL=$azureStaticWebAppUrl
|
||||
AZURE_PRODUCTION_URL=$productionUrl
|
||||
"@
|
||||
|
||||
$configContent | Out-File -FilePath ".azure-entra-config.env" -Encoding UTF8
|
||||
Write-Host "✅ Configuration saved to .azure-entra-config.env" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MS Entra (Azure AD) Setup Script for Miracles In Motion
|
||||
# This script helps configure Azure AD authentication for the application
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
APP_NAME="Miracles In Motion Web App"
|
||||
DOMAIN="miraclesinmotion.org"
|
||||
STATIC_WEB_APP_NAME="${STATIC_WEB_APP_NAME:-mim-prod-web}"
|
||||
AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-rg-miraclesinmotion-prod}"
|
||||
|
||||
echo -e "${GREEN}🔐 MS Entra (Azure AD) Setup Script${NC}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check if Azure CLI is installed
|
||||
if ! command -v az &> /dev/null; then
|
||||
echo -e "${RED}❌ Azure CLI not found. Please install it first.${NC}"
|
||||
echo "Install from: https://docs.microsoft.com/cli/azure/install-azure-cli"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if logged in to Azure
|
||||
echo -e "${BLUE}📋 Checking Azure login status...${NC}"
|
||||
CURRENT_USER=$(az account show --query "user.name" -o tsv 2>/dev/null || echo "")
|
||||
if [ -z "$CURRENT_USER" ]; then
|
||||
echo -e "${YELLOW}⚠️ Not logged in to Azure. Please log in...${NC}"
|
||||
az login
|
||||
CURRENT_USER=$(az account show --query "user.name" -o tsv)
|
||||
fi
|
||||
echo -e "${GREEN}✅ Logged in as: $CURRENT_USER${NC}"
|
||||
echo ""
|
||||
|
||||
# Get Azure Static Web App URL
|
||||
echo -e "${BLUE}📋 Getting Azure Static Web App information...${NC}"
|
||||
AZURE_STATIC_WEB_APP_URL=$(az staticwebapp show \
|
||||
--name "$STATIC_WEB_APP_NAME" \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--query "defaultHostname" -o tsv 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$AZURE_STATIC_WEB_APP_URL" ]; then
|
||||
echo -e "${YELLOW}⚠️ Static Web App not found. Using default URL format.${NC}"
|
||||
AZURE_STATIC_WEB_APP_URL="${STATIC_WEB_APP_NAME}.azurestaticapps.net"
|
||||
fi
|
||||
|
||||
FULL_STATIC_WEB_APP_URL="https://${AZURE_STATIC_WEB_APP_URL}"
|
||||
PRODUCTION_URL="https://${DOMAIN}"
|
||||
WWW_URL="https://www.${DOMAIN}"
|
||||
|
||||
echo -e "${GREEN}✅ Static Web App URL: $FULL_STATIC_WEB_APP_URL${NC}"
|
||||
echo ""
|
||||
|
||||
# Get Tenant ID
|
||||
TENANT_ID=$(az account show --query "tenantId" -o tsv)
|
||||
echo -e "${GREEN}✅ Tenant ID: $TENANT_ID${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if app registration already exists
|
||||
echo -e "${BLUE}🔍 Checking for existing app registration...${NC}"
|
||||
EXISTING_APP_ID=$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$EXISTING_APP_ID" ] && [ "$EXISTING_APP_ID" != "null" ]; then
|
||||
echo -e "${YELLOW}⚠️ App registration already exists: $EXISTING_APP_ID${NC}"
|
||||
read -p "Do you want to update it? (y/n): " UPDATE_APP
|
||||
if [ "$UPDATE_APP" != "y" ]; then
|
||||
APP_ID=$EXISTING_APP_ID
|
||||
echo -e "${GREEN}✅ Using existing app registration${NC}"
|
||||
else
|
||||
echo -e "${BLUE}📝 Updating app registration...${NC}"
|
||||
APP_ID=$EXISTING_APP_ID
|
||||
fi
|
||||
else
|
||||
# Create app registration
|
||||
echo -e "${BLUE}📝 Creating app registration...${NC}"
|
||||
APP_ID=$(az ad app create \
|
||||
--display-name "$APP_NAME" \
|
||||
--sign-in-audience "AzureADMultipleOrgs" \
|
||||
--web-redirect-uris "$PRODUCTION_URL" "$WWW_URL" "$FULL_STATIC_WEB_APP_URL" \
|
||||
--query "appId" -o tsv)
|
||||
|
||||
echo -e "${GREEN}✅ App registration created: $APP_ID${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Update redirect URIs
|
||||
echo -e "${BLUE}📝 Updating redirect URIs...${NC}"
|
||||
az ad app update --id "$APP_ID" \
|
||||
--web-redirect-uris "$PRODUCTION_URL" "$WWW_URL" "$FULL_STATIC_WEB_APP_URL" \
|
||||
--enable-id-token-issuance true \
|
||||
--enable-access-token-issuance false \
|
||||
--query "appId" -o tsv > /dev/null
|
||||
|
||||
echo -e "${GREEN}✅ Redirect URIs updated${NC}"
|
||||
echo " - $PRODUCTION_URL"
|
||||
echo " - $WWW_URL"
|
||||
echo " - $FULL_STATIC_WEB_APP_URL"
|
||||
echo ""
|
||||
|
||||
# Configure API permissions
|
||||
echo -e "${BLUE}📝 Configuring API permissions...${NC}"
|
||||
|
||||
# Microsoft Graph permissions
|
||||
GRAPH_PERMISSIONS=(
|
||||
"User.Read"
|
||||
"User.ReadBasic.All"
|
||||
"email"
|
||||
"openid"
|
||||
"profile"
|
||||
)
|
||||
|
||||
GRAPH_RESOURCE_ID=$(az ad sp show --id "00000003-0000-0000-c000-000000000000" --query "id" -o tsv)
|
||||
|
||||
for PERMISSION in "${GRAPH_PERMISSIONS[@]}"; do
|
||||
PERMISSION_ID=$(az ad sp show --id "00000003-0000-0000-c000-000000000000" --query "oauth2PermissionScopes[?value=='$PERMISSION'].id" -o tsv)
|
||||
if [ -n "$PERMISSION_ID" ]; then
|
||||
echo " Adding permission: $PERMISSION"
|
||||
az ad app permission add \
|
||||
--id "$APP_ID" \
|
||||
--api "00000003-0000-0000-c000-000000000000" \
|
||||
--api-permissions "$PERMISSION_ID=Scope" \
|
||||
--query "appId" -o tsv > /dev/null 2>&1 || echo " (may already exist)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${GREEN}✅ API permissions configured${NC}"
|
||||
echo ""
|
||||
|
||||
# Create app roles
|
||||
echo -e "${BLUE}📝 Creating app roles...${NC}"
|
||||
|
||||
# Function to create app role
|
||||
create_app_role() {
|
||||
local role_name=$1
|
||||
local role_value=$2
|
||||
local role_description=$3
|
||||
|
||||
echo " Creating role: $role_name"
|
||||
|
||||
local role_json=$(cat <<EOF
|
||||
{
|
||||
"allowedMemberTypes": ["User"],
|
||||
"description": "$role_description",
|
||||
"displayName": "$role_name",
|
||||
"id": "$(uuidgen | tr '[:upper:]' '[:lower:]')",
|
||||
"isEnabled": true,
|
||||
"value": "$role_value"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Get existing roles
|
||||
local existing_roles=$(az ad app show --id "$APP_ID" --query "appRoles" -o json)
|
||||
|
||||
# Check if role already exists
|
||||
local role_exists=$(echo "$existing_roles" | jq -r ".[] | select(.value == \"$role_value\") | .value" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$role_exists" ]; then
|
||||
# Add new role to existing roles
|
||||
local updated_roles=$(echo "$existing_roles" | jq ". + [$role_json]")
|
||||
az ad app update --id "$APP_ID" --app-roles "$updated_roles" > /dev/null 2>&1
|
||||
echo " ✅ Role created"
|
||||
else
|
||||
echo " ⚠️ Role already exists"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create app roles
|
||||
create_app_role "Admin" "Admin" "Administrator access to all features"
|
||||
create_app_role "Volunteer" "Volunteer" "Volunteer access to assigned tasks"
|
||||
create_app_role "Resource" "Resource" "Resource provider access"
|
||||
|
||||
echo -e "${GREEN}✅ App roles created${NC}"
|
||||
echo ""
|
||||
|
||||
# Create service principal
|
||||
echo -e "${BLUE}📝 Creating service principal...${NC}"
|
||||
SP_ID=$(az ad sp create --id "$APP_ID" --query "id" -o tsv 2>/dev/null || \
|
||||
az ad sp show --id "$APP_ID" --query "id" -o tsv)
|
||||
|
||||
echo -e "${GREEN}✅ Service principal created: $SP_ID${NC}"
|
||||
echo ""
|
||||
|
||||
# Grant admin consent (requires admin privileges)
|
||||
echo -e "${BLUE}📝 Granting admin consent for API permissions...${NC}"
|
||||
read -p "Do you have admin privileges to grant consent? (y/n): " HAS_ADMIN
|
||||
|
||||
if [ "$HAS_ADMIN" == "y" ]; then
|
||||
az ad app permission admin-consent --id "$APP_ID" && \
|
||||
echo -e "${GREEN}✅ Admin consent granted${NC}" || \
|
||||
echo -e "${YELLOW}⚠️ Could not grant admin consent. You may need to do this manually.${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Skipping admin consent. Please grant consent manually in Azure Portal.${NC}"
|
||||
echo " Go to: Azure Portal → Microsoft Entra ID → App registrations → $APP_NAME → API permissions → Grant admin consent"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Create client secret (optional)
|
||||
echo -e "${BLUE}📝 Client Secret Configuration...${NC}"
|
||||
read -p "Do you want to create a client secret? (y/n): " CREATE_SECRET
|
||||
|
||||
if [ "$CREATE_SECRET" == "y" ]; then
|
||||
SECRET_NAME="Miracles In Motion Secret $(date +%Y%m%d)"
|
||||
SECRET=$(az ad app credential reset --id "$APP_ID" --display-name "$SECRET_NAME" --years 2 --query "password" -o tsv)
|
||||
echo -e "${GREEN}✅ Client secret created${NC}"
|
||||
echo -e "${RED}⚠️ IMPORTANT: Save this secret now - it won't be shown again!${NC}"
|
||||
echo "Secret: $SECRET"
|
||||
echo ""
|
||||
read -p "Press Enter to continue after saving the secret..."
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Skipping client secret creation${NC}"
|
||||
SECRET=""
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Store configuration in Key Vault (if available)
|
||||
echo -e "${BLUE}📝 Storing configuration in Key Vault...${NC}"
|
||||
KEY_VAULT_NAME=$(az keyvault list --resource-group "$AZURE_RESOURCE_GROUP" --query "[0].name" -o tsv 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$KEY_VAULT_NAME" ]; then
|
||||
echo "Storing in Key Vault: $KEY_VAULT_NAME"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name "$KEY_VAULT_NAME" \
|
||||
--name "azure-client-id" \
|
||||
--value "$APP_ID" > /dev/null 2>&1 && echo -e "${GREEN}✅ Client ID stored${NC}" || echo -e "${YELLOW}⚠️ Could not store Client ID${NC}"
|
||||
|
||||
az keyvault secret set \
|
||||
--vault-name "$KEY_VAULT_NAME" \
|
||||
--name "azure-tenant-id" \
|
||||
--value "$TENANT_ID" > /dev/null 2>&1 && echo -e "${GREEN}✅ Tenant ID stored${NC}" || echo -e "${YELLOW}⚠️ Could not store Tenant ID${NC}"
|
||||
|
||||
if [ -n "$SECRET" ]; then
|
||||
az keyvault secret set \
|
||||
--vault-name "$KEY_VAULT_NAME" \
|
||||
--name "azure-client-secret" \
|
||||
--value "$SECRET" > /dev/null 2>&1 && echo -e "${GREEN}✅ Client Secret stored${NC}" || echo -e "${YELLOW}⚠️ Could not store Client Secret${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Key Vault not found. Skipping secret storage.${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo -e "${GREEN}✅ MS Entra Setup Complete!${NC}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
echo "Configuration Summary:"
|
||||
echo " App Registration ID: $APP_ID"
|
||||
echo " Tenant ID: $TENANT_ID"
|
||||
echo " Service Principal ID: $SP_ID"
|
||||
echo ""
|
||||
echo "Redirect URIs:"
|
||||
echo " - $PRODUCTION_URL"
|
||||
echo " - $WWW_URL"
|
||||
echo " - $FULL_STATIC_WEB_APP_URL"
|
||||
echo ""
|
||||
echo "App Roles:"
|
||||
echo " - Admin"
|
||||
echo " - Volunteer"
|
||||
echo " - Resource"
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo "1. Assign users to app roles in Azure Portal"
|
||||
echo "2. Update staticwebapp.config.json with authentication configuration"
|
||||
echo "3. Update application code to use Azure AD authentication"
|
||||
echo "4. Test authentication flow"
|
||||
echo ""
|
||||
echo "Azure Portal Links:"
|
||||
echo " App Registration: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$APP_ID"
|
||||
echo " API Permissions: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$APP_ID"
|
||||
echo " App Roles: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/AppRoles/appId/$APP_ID"
|
||||
echo ""
|
||||
|
||||
# Export variables for use in other scripts
|
||||
cat > .azure-entra-config.env <<EOF
|
||||
# Azure AD Configuration
|
||||
AZURE_CLIENT_ID=$APP_ID
|
||||
AZURE_TENANT_ID=$TENANT_ID
|
||||
AZURE_CLIENT_SECRET=$SECRET
|
||||
AZURE_STATIC_WEB_APP_URL=$FULL_STATIC_WEB_APP_URL
|
||||
AZURE_PRODUCTION_URL=$PRODUCTION_URL
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✅ Configuration saved to .azure-entra-config.env${NC}"
|
||||
echo ""
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Automated Cloudflare Setup Script
|
||||
# Reads credentials from .env.production and configures Cloudflare automatically
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
DOMAIN="mim4u.org"
|
||||
STATIC_WEB_APP_NAME="mim-prod-igiay4-web"
|
||||
AZURE_RESOURCE_GROUP="rg-miraclesinmotion-prod"
|
||||
|
||||
echo -e "${GREEN}🌐 Automated Cloudflare Setup${NC}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Load environment variables from .env files
|
||||
ENV_FILES=(".env.production" ".env" "../.env.production" "../.env")
|
||||
CREDENTIALS_LOADED=false
|
||||
|
||||
for env_file in "${ENV_FILES[@]}"; do
|
||||
if [ -f "$env_file" ]; then
|
||||
echo -e "${GREEN}📋 Loading credentials from $env_file...${NC}"
|
||||
# Try different formats
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^CLOUDFLARE_API_TOKEN= ]] || [[ "$line" =~ ^CLOUDFLARE_ZONE_ID= ]]; then
|
||||
export "$line"
|
||||
CREDENTIALS_LOADED=true
|
||||
fi
|
||||
done < "$env_file"
|
||||
|
||||
# Also try with export command
|
||||
set -a
|
||||
source "$env_file" 2>/dev/null || true
|
||||
set +a
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if credentials are already set in environment
|
||||
if [ -n "$CLOUDFLARE_API_TOKEN" ] && [ -n "$CLOUDFLARE_ZONE_ID" ]; then
|
||||
CREDENTIALS_LOADED=true
|
||||
fi
|
||||
|
||||
# Check if credentials are set
|
||||
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ZONE_ID" ]; then
|
||||
echo -e "${YELLOW}⚠️ Cloudflare credentials not found in env files${NC}"
|
||||
echo "Checking environment variables..."
|
||||
|
||||
# Final check - maybe they're already exported
|
||||
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ZONE_ID" ]; then
|
||||
echo -e "${RED}❌ Cloudflare credentials not found${NC}"
|
||||
echo "Please set: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID"
|
||||
echo "Or add them to .env.production file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Credentials loaded${NC}"
|
||||
echo "Zone ID: ${CLOUDFLARE_ZONE_ID:0:15}..."
|
||||
echo ""
|
||||
|
||||
# Get Azure Static Web App default hostname
|
||||
echo -e "${GREEN}📋 Getting Azure Static Web App information...${NC}"
|
||||
AZURE_STATIC_WEB_APP_URL=$(az staticwebapp show \
|
||||
--name "$STATIC_WEB_APP_NAME" \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--query "defaultHostname" -o tsv 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$AZURE_STATIC_WEB_APP_URL" ]; then
|
||||
echo -e "${RED}❌ Could not find Static Web App${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Found Static Web App: ${AZURE_STATIC_WEB_APP_URL}${NC}"
|
||||
echo ""
|
||||
|
||||
# Verify Cloudflare API access
|
||||
echo -e "${GREEN}🔐 Verifying Cloudflare API access...${NC}"
|
||||
ZONE_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
ZONE_SUCCESS=$(echo "$ZONE_RESPONSE" | grep -o '"success":true' || echo "")
|
||||
|
||||
if [ -z "$ZONE_SUCCESS" ]; then
|
||||
echo -e "${RED}❌ Failed to authenticate with Cloudflare API${NC}"
|
||||
echo "Response: $ZONE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ZONE_NAME=$(echo "$ZONE_RESPONSE" | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
|
||||
echo -e "${GREEN}✅ Authenticated with Cloudflare${NC}"
|
||||
echo "Zone: $ZONE_NAME"
|
||||
echo ""
|
||||
|
||||
# Function to create or update DNS record
|
||||
create_dns_record() {
|
||||
local record_type=$1
|
||||
local record_name=$2
|
||||
local record_content=$3
|
||||
local proxy=$4
|
||||
|
||||
echo -n "Configuring DNS: $record_name.$DOMAIN -> $record_content... "
|
||||
|
||||
# Check if record exists
|
||||
EXISTING_RECORD=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/dns_records?type=$record_type&name=$record_name.$DOMAIN" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
RECORD_ID=$(echo "$EXISTING_RECORD" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$RECORD_ID" ] && [ "$RECORD_ID" != "null" ]; then
|
||||
# Update existing record
|
||||
DATA=$(cat <<EOF
|
||||
{
|
||||
"type": "$record_type",
|
||||
"name": "$record_name",
|
||||
"content": "$record_content",
|
||||
"proxied": $proxy,
|
||||
"ttl": 1
|
||||
}
|
||||
EOF
|
||||
)
|
||||
RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/dns_records/$RECORD_ID" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$DATA")
|
||||
else
|
||||
# Create new record
|
||||
DATA=$(cat <<EOF
|
||||
{
|
||||
"type": "$record_type",
|
||||
"name": "$record_name",
|
||||
"content": "$record_content",
|
||||
"proxied": $proxy,
|
||||
"ttl": 1
|
||||
}
|
||||
EOF
|
||||
)
|
||||
RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/dns_records" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$DATA")
|
||||
fi
|
||||
|
||||
SUCCESS=$(echo "$RESPONSE" | grep -o '"success":true' || echo "")
|
||||
if [ -n "$SUCCESS" ]; then
|
||||
echo -e "${GREEN}✅${NC}"
|
||||
return 0
|
||||
else
|
||||
ERRORS=$(echo "$RESPONSE" | grep -o '"message":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||||
echo -e "${YELLOW}⚠️ $ERRORS${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create DNS records
|
||||
echo -e "${GREEN}📝 Configuring DNS Records...${NC}"
|
||||
create_dns_record "CNAME" "www" "$AZURE_STATIC_WEB_APP_URL" "true"
|
||||
create_dns_record "CNAME" "@" "$AZURE_STATIC_WEB_APP_URL" "true"
|
||||
echo ""
|
||||
|
||||
# Configure SSL/TLS settings
|
||||
echo -e "${GREEN}🔒 Configuring SSL/TLS...${NC}"
|
||||
|
||||
# Set SSL mode to Full
|
||||
SSL_RESPONSE=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/settings/ssl" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"full"}')
|
||||
|
||||
if echo "$SSL_RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✅ SSL mode set to Full${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Could not update SSL settings${NC}"
|
||||
fi
|
||||
|
||||
# Enable Always Use HTTPS
|
||||
HTTPS_RESPONSE=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/settings/always_use_https" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"on"}')
|
||||
|
||||
if echo "$HTTPS_RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✅ Always Use HTTPS enabled${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Could not enable Always Use HTTPS${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Configure Security Settings
|
||||
echo -e "${GREEN}🛡️ Configuring Security Settings...${NC}"
|
||||
|
||||
# Set security level to Medium
|
||||
SECURITY_RESPONSE=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/settings/security_level" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"medium"}')
|
||||
|
||||
if echo "$SECURITY_RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✅ Security level set to Medium${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Could not update security level${NC}"
|
||||
fi
|
||||
|
||||
# Enable Browser Integrity Check
|
||||
BROWSER_RESPONSE=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/settings/browser_check" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"on"}')
|
||||
|
||||
if echo "$BROWSER_RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✅ Browser Integrity Check enabled${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Could not enable browser check${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Configure Speed Settings
|
||||
echo -e "${GREEN}⚡ Configuring Speed Settings...${NC}"
|
||||
|
||||
# Enable Minification
|
||||
MINIFY_RESPONSE=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/settings/minify" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":{"css":"on","html":"on","js":"on"}}')
|
||||
|
||||
if echo "$MINIFY_RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✅ Minification enabled${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Could not enable minification${NC}"
|
||||
fi
|
||||
|
||||
# Enable Brotli compression
|
||||
BROTLI_RESPONSE=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/settings/brotli" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"on"}')
|
||||
|
||||
if echo "$BROTLI_RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✅ Brotli compression enabled${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Could not enable Brotli${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Add custom domain to Azure Static Web App
|
||||
echo -e "${GREEN}🔗 Adding Custom Domain to Azure Static Web App...${NC}"
|
||||
|
||||
# For apex domain (may require TXT validation)
|
||||
az staticwebapp hostname set \
|
||||
--name "$STATIC_WEB_APP_NAME" \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--hostname "$DOMAIN" 2>/dev/null && \
|
||||
echo -e "${GREEN}✅ Custom domain $DOMAIN added${NC}" || \
|
||||
echo -e "${YELLOW}⚠️ Domain may already be added or DNS not ready${NC}"
|
||||
|
||||
# For www subdomain
|
||||
az staticwebapp hostname set \
|
||||
--name "$STATIC_WEB_APP_NAME" \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--hostname "www.$DOMAIN" 2>/dev/null && \
|
||||
echo -e "${GREEN}✅ Custom domain www.$DOMAIN added${NC}" || \
|
||||
echo -e "${YELLOW}⚠️ Domain may already be added or DNS not ready${NC}"
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo -e "${GREEN}✅ Cloudflare Setup Complete!${NC}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
echo "Configuration Summary:"
|
||||
echo " Domain: $DOMAIN"
|
||||
echo " Static Web App: $AZURE_STATIC_WEB_APP_URL"
|
||||
echo ""
|
||||
echo "DNS Records:"
|
||||
echo " ✅ www.$DOMAIN -> $AZURE_STATIC_WEB_APP_URL (Proxied)"
|
||||
echo " ✅ $DOMAIN -> $AZURE_STATIC_WEB_APP_URL (Proxied)"
|
||||
echo ""
|
||||
echo "Cloudflare Settings:"
|
||||
echo " ✅ SSL Mode: Full"
|
||||
echo " ✅ Always Use HTTPS: Enabled"
|
||||
echo " ✅ Security Level: Medium"
|
||||
echo " ✅ Browser Integrity Check: Enabled"
|
||||
echo " ✅ Minification: Enabled (JS, CSS, HTML)"
|
||||
echo " ✅ Brotli Compression: Enabled"
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo " 1. Wait for DNS propagation (usually 5-30 minutes)"
|
||||
echo " 2. Verify SSL certificates are provisioned (1-24 hours)"
|
||||
echo " 3. Test the website at https://$DOMAIN"
|
||||
echo " 4. Monitor Cloudflare analytics"
|
||||
echo ""
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
# Cloudflare Setup Script for Miracles In Motion (PowerShell)
|
||||
# This script helps configure Cloudflare for the production deployment
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Domain = "mim4u.org",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$StaticWebAppName = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$AzureResourceGroup = "rg-miraclesinmotion-prod",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CloudflareApiToken = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CloudflareZoneId = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "🌐 Cloudflare Setup Script" -ForegroundColor Green
|
||||
Write-Host "==================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check if Azure CLI is installed
|
||||
if (-not (Get-Command "az" -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "❌ Azure CLI not found. Please install it first." -ForegroundColor Red
|
||||
Write-Host "Install from: https://docs.microsoft.com/cli/azure/install-azure-cli" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get Azure Static Web App default hostname
|
||||
Write-Host "📋 Getting Azure Static Web App information..." -ForegroundColor Cyan
|
||||
|
||||
if ([string]::IsNullOrEmpty($StaticWebAppName)) {
|
||||
# Try to find Static Web App
|
||||
$swa = az staticwebapp list --resource-group $AzureResourceGroup --output json | ConvertFrom-Json | Select-Object -First 1
|
||||
if ($swa) {
|
||||
$StaticWebAppName = $swa.name
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrEmpty($StaticWebAppName)) {
|
||||
Write-Host "❌ Static Web App name not specified and could not be found." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$azureStaticWebAppUrl = az staticwebapp show `
|
||||
--name $StaticWebAppName `
|
||||
--resource-group $AzureResourceGroup `
|
||||
--query "defaultHostname" -o tsv
|
||||
|
||||
if ([string]::IsNullOrEmpty($azureStaticWebAppUrl)) {
|
||||
Write-Host "❌ Could not find Static Web App." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Found Static Web App: $azureStaticWebAppUrl" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Get Cloudflare API Token
|
||||
if ([string]::IsNullOrEmpty($CloudflareApiToken)) {
|
||||
$CloudflareApiToken = Read-Host "Enter your Cloudflare API Token"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrEmpty($CloudflareApiToken)) {
|
||||
Write-Host "❌ Cloudflare API Token is required." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get Cloudflare Zone ID
|
||||
if ([string]::IsNullOrEmpty($CloudflareZoneId)) {
|
||||
Write-Host "Looking up Zone ID for $Domain..." -ForegroundColor Cyan
|
||||
|
||||
$headers = @{
|
||||
"Authorization" = "Bearer $CloudflareApiToken"
|
||||
"Content-Type" = "application/json"
|
||||
}
|
||||
|
||||
$zoneResponse = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones?name=$Domain" -Method Get -Headers $headers
|
||||
|
||||
if ($zoneResponse.success -and $zoneResponse.result.Count -gt 0) {
|
||||
$CloudflareZoneId = $zoneResponse.result[0].id
|
||||
Write-Host "✅ Zone ID: $CloudflareZoneId" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Could not find Zone ID for $Domain" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Function to create DNS record
|
||||
function New-CloudflareDnsRecord {
|
||||
param(
|
||||
[string]$RecordType,
|
||||
[string]$RecordName,
|
||||
[string]$RecordContent,
|
||||
[bool]$Proxied = $true
|
||||
)
|
||||
|
||||
Write-Host "Creating DNS record: $RecordName.$Domain -> $RecordContent" -ForegroundColor Cyan
|
||||
|
||||
$headers = @{
|
||||
"Authorization" = "Bearer $CloudflareApiToken"
|
||||
"Content-Type" = "application/json"
|
||||
}
|
||||
|
||||
$body = @{
|
||||
type = $RecordType
|
||||
name = $RecordName
|
||||
content = $RecordContent
|
||||
proxied = $Proxied
|
||||
ttl = 1
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$CloudflareZoneId/dns_records" -Method Post -Headers $headers -Body $body
|
||||
|
||||
if ($response.success) {
|
||||
Write-Host "✅ DNS record created successfully" -ForegroundColor Green
|
||||
return $true
|
||||
} else {
|
||||
$errors = $response.errors | ForEach-Object { $_.message } -Join ", "
|
||||
Write-Host "⚠️ DNS record may already exist or error: $errors" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Error creating DNS record: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# Create CNAME records
|
||||
Write-Host "📝 Creating DNS Records..." -ForegroundColor Green
|
||||
New-CloudflareDnsRecord -RecordType "CNAME" -RecordName "www" -RecordContent $azureStaticWebAppUrl -Proxied $true
|
||||
New-CloudflareDnsRecord -RecordType "CNAME" -RecordName "@" -RecordContent $azureStaticWebAppUrl -Proxied $true
|
||||
Write-Host ""
|
||||
|
||||
# Configure SSL/TLS settings
|
||||
Write-Host "🔒 Configuring SSL/TLS..." -ForegroundColor Green
|
||||
|
||||
$headers = @{
|
||||
"Authorization" = "Bearer $CloudflareApiToken"
|
||||
"Content-Type" = "application/json"
|
||||
}
|
||||
|
||||
$sslBody = @{
|
||||
value = "full"
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$sslResponse = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$CloudflareZoneId/settings/ssl" -Method Patch -Headers $headers -Body $sslBody
|
||||
if ($sslResponse.success) {
|
||||
Write-Host "✅ SSL mode set to Full" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Could not update SSL settings: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$httpsBody = @{
|
||||
value = "on"
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$httpsResponse = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$CloudflareZoneId/settings/always_use_https" -Method Patch -Headers $headers -Body $httpsBody
|
||||
if ($httpsResponse.success) {
|
||||
Write-Host "✅ Always Use HTTPS enabled" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Could not enable Always Use HTTPS: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Configure Security Settings
|
||||
Write-Host "🛡️ Configuring Security Settings..." -ForegroundColor Green
|
||||
|
||||
$securityBody = @{
|
||||
value = "medium"
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$securityResponse = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$CloudflareZoneId/settings/security_level" -Method Patch -Headers $headers -Body $securityBody
|
||||
if ($securityResponse.success) {
|
||||
Write-Host "✅ Security level set to Medium" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Could not update security level: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$browserCheckBody = @{
|
||||
value = "on"
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$browserCheckResponse = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$CloudflareZoneId/settings/browser_check" -Method Patch -Headers $headers -Body $browserCheckBody
|
||||
if ($browserCheckResponse.success) {
|
||||
Write-Host "✅ Browser Integrity Check enabled" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Could not enable browser check: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Configure Speed Settings
|
||||
Write-Host "⚡ Configuring Speed Settings..." -ForegroundColor Green
|
||||
|
||||
$minifyBody = @{
|
||||
value = @{
|
||||
css = "on"
|
||||
html = "on"
|
||||
js = "on"
|
||||
}
|
||||
} | ConvertTo-Json -Depth 3
|
||||
|
||||
try {
|
||||
$minifyResponse = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$CloudflareZoneId/settings/minify" -Method Patch -Headers $headers -Body $minifyBody
|
||||
if ($minifyResponse.success) {
|
||||
Write-Host "✅ Minification enabled" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Could not enable minification: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$brotliBody = @{
|
||||
value = "on"
|
||||
} | ConvertTo-Json
|
||||
|
||||
try {
|
||||
$brotliResponse = Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$CloudflareZoneId/settings/brotli" -Method Patch -Headers $headers -Body $brotliBody
|
||||
if ($brotliResponse.success) {
|
||||
Write-Host "✅ Brotli compression enabled" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Could not enable Brotli: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Add custom domain to Azure Static Web App
|
||||
Write-Host "🔗 Adding Custom Domain to Azure Static Web App..." -ForegroundColor Green
|
||||
|
||||
try {
|
||||
az staticwebapp hostname set `
|
||||
--name $StaticWebAppName `
|
||||
--resource-group $AzureResourceGroup `
|
||||
--hostname $Domain 2>$null | Out-Null
|
||||
Write-Host "✅ Custom domain $Domain added" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "⚠️ Domain may already be added or DNS not ready" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
try {
|
||||
az staticwebapp hostname set `
|
||||
--name $StaticWebAppName `
|
||||
--resource-group $AzureResourceGroup `
|
||||
--hostname "www.$Domain" 2>$null | Out-Null
|
||||
Write-Host "✅ Custom domain www.$Domain added" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "⚠️ Domain may already be added or DNS not ready" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Summary
|
||||
Write-Host "✅ Cloudflare Setup Complete!" -ForegroundColor Green
|
||||
Write-Host "==================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next Steps:"
|
||||
Write-Host "1. Verify DNS propagation (may take 24-48 hours)"
|
||||
Write-Host "2. Verify SSL certificates are provisioned"
|
||||
Write-Host "3. Test the website at https://$Domain"
|
||||
Write-Host "4. Monitor Cloudflare analytics"
|
||||
Write-Host ""
|
||||
Write-Host "DNS Records Created:"
|
||||
Write-Host " - www.$Domain -> $azureStaticWebAppUrl"
|
||||
Write-Host " - $Domain -> $azureStaticWebAppUrl"
|
||||
Write-Host ""
|
||||
Write-Host "Cloudflare Settings:"
|
||||
Write-Host " - SSL Mode: Full (strict)"
|
||||
Write-Host " - Always Use HTTPS: Enabled"
|
||||
Write-Host " - Security Level: Medium"
|
||||
Write-Host " - Minification: Enabled"
|
||||
Write-Host " - Brotli Compression: Enabled"
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Cloudflare Setup Script for Miracles In Motion
|
||||
# This script helps configure Cloudflare for the production deployment
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
DOMAIN="miraclesinmotion.org"
|
||||
STATIC_WEB_APP_NAME="${STATIC_WEB_APP_NAME:-mim-prod-web}"
|
||||
AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-rg-miraclesinmotion-prod}"
|
||||
|
||||
echo -e "${GREEN}🌐 Cloudflare Setup Script${NC}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Check if Cloudflare CLI is installed
|
||||
if ! command -v cloudflared &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠️ Cloudflare CLI (cloudflared) not found.${NC}"
|
||||
echo "Install it from: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠️ jq not found. Installing...${NC}"
|
||||
# Try to install jq (this may vary by OS)
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
sudo apt-get update && sudo apt-get install -y jq
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
brew install jq
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get Azure Static Web App default hostname
|
||||
echo -e "${GREEN}📋 Getting Azure Static Web App information...${NC}"
|
||||
AZURE_STATIC_WEB_APP_URL=$(az staticwebapp show \
|
||||
--name "$STATIC_WEB_APP_NAME" \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--query "defaultHostname" -o tsv 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$AZURE_STATIC_WEB_APP_URL" ]; then
|
||||
echo -e "${RED}❌ Could not find Static Web App. Please check the name and resource group.${NC}"
|
||||
echo "Usage: STATIC_WEB_APP_NAME=your-app-name AZURE_RESOURCE_GROUP=your-rg ./setup-cloudflare.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Found Static Web App: ${AZURE_STATIC_WEB_APP_URL}${NC}"
|
||||
echo ""
|
||||
|
||||
# Prompt for Cloudflare API credentials
|
||||
echo -e "${YELLOW}📝 Cloudflare API Configuration${NC}"
|
||||
echo "You need a Cloudflare API token with the following permissions:"
|
||||
echo " - Zone:Read, DNS:Edit, SSL:Edit, Page Rules:Edit"
|
||||
echo ""
|
||||
read -p "Enter your Cloudflare API Token: " CF_API_TOKEN
|
||||
read -p "Enter your Cloudflare Zone ID (or we'll look it up): " CF_ZONE_ID
|
||||
|
||||
if [ -z "$CF_ZONE_ID" ]; then
|
||||
echo "Looking up Zone ID for $DOMAIN..."
|
||||
CF_ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" | jq -r '.result[0].id')
|
||||
|
||||
if [ -z "$CF_ZONE_ID" ] || [ "$CF_ZONE_ID" == "null" ]; then
|
||||
echo -e "${RED}❌ Could not find Zone ID for $DOMAIN${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Zone ID: $CF_ZONE_ID${NC}"
|
||||
echo ""
|
||||
|
||||
# Function to create DNS record
|
||||
create_dns_record() {
|
||||
local record_type=$1
|
||||
local record_name=$2
|
||||
local record_content=$3
|
||||
local proxy=$4
|
||||
|
||||
echo "Creating DNS record: $record_name.$DOMAIN -> $record_content"
|
||||
|
||||
local data=$(jq -n \
|
||||
--arg type "$record_type" \
|
||||
--arg name "$record_name" \
|
||||
--arg content "$record_content" \
|
||||
--argjson proxied "$proxy" \
|
||||
'{
|
||||
type: $type,
|
||||
name: $name,
|
||||
content: $content,
|
||||
proxied: $proxied,
|
||||
ttl: 1
|
||||
}')
|
||||
|
||||
local response=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$data")
|
||||
|
||||
local success=$(echo "$response" | jq -r '.success')
|
||||
if [ "$success" == "true" ]; then
|
||||
echo -e "${GREEN}✅ DNS record created successfully${NC}"
|
||||
else
|
||||
local errors=$(echo "$response" | jq -r '.errors[]?.message' | tr '\n' ' ')
|
||||
echo -e "${YELLOW}⚠️ DNS record may already exist or error: $errors${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create CNAME records
|
||||
echo -e "${GREEN}📝 Creating DNS Records...${NC}"
|
||||
create_dns_record "CNAME" "www" "$AZURE_STATIC_WEB_APP_URL" "true"
|
||||
create_dns_record "CNAME" "@" "$AZURE_STATIC_WEB_APP_URL" "true"
|
||||
echo ""
|
||||
|
||||
# Configure SSL/TLS settings
|
||||
echo -e "${GREEN}🔒 Configuring SSL/TLS...${NC}"
|
||||
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/ssl" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"full"}' | jq -r '.success' && echo -e "${GREEN}✅ SSL mode set to Full${NC}" || echo -e "${YELLOW}⚠️ Could not update SSL settings${NC}"
|
||||
|
||||
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/always_use_https" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"on"}' | jq -r '.success' && echo -e "${GREEN}✅ Always Use HTTPS enabled${NC}" || echo -e "${YELLOW}⚠️ Could not enable Always Use HTTPS${NC}"
|
||||
echo ""
|
||||
|
||||
# Create Page Rules
|
||||
echo -e "${GREEN}📋 Creating Page Rules...${NC}"
|
||||
|
||||
# Rule 1: Force HTTPS
|
||||
create_page_rule() {
|
||||
local url=$1
|
||||
local settings=$2
|
||||
local rule_name=$3
|
||||
|
||||
local data=$(jq -n \
|
||||
--arg url "$url" \
|
||||
--argjson settings "$settings" \
|
||||
'{
|
||||
targets: [
|
||||
{
|
||||
target: "url",
|
||||
constraint: {
|
||||
operator: "matches",
|
||||
value: $url
|
||||
}
|
||||
}
|
||||
],
|
||||
actions: $settings,
|
||||
priority: 1,
|
||||
status: "active"
|
||||
}')
|
||||
|
||||
local response=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/pagerules" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$data")
|
||||
|
||||
local success=$(echo "$response" | jq -r '.success')
|
||||
if [ "$success" == "true" ]; then
|
||||
echo -e "${GREEN}✅ Page rule '$rule_name' created${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Could not create page rule '$rule_name'${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create page rules
|
||||
HTTPS_SETTINGS='[{"id": "always_use_https"}]'
|
||||
create_page_rule "*${DOMAIN}/*" "$HTTPS_SETTINGS" "Force HTTPS"
|
||||
echo ""
|
||||
|
||||
# Configure Security Settings
|
||||
echo -e "${GREEN}🛡️ Configuring Security Settings...${NC}"
|
||||
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/security_level" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"medium"}' | jq -r '.success' && echo -e "${GREEN}✅ Security level set to Medium${NC}" || echo -e "${YELLOW}⚠️ Could not update security level${NC}"
|
||||
|
||||
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/browser_check" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"on"}' | jq -r '.success' && echo -e "${GREEN}✅ Browser Integrity Check enabled${NC}" || echo -e "${YELLOW}⚠️ Could not enable browser check${NC}"
|
||||
echo ""
|
||||
|
||||
# Configure Speed Settings
|
||||
echo -e "${GREEN}⚡ Configuring Speed Settings...${NC}"
|
||||
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/minify" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":{"css":"on","html":"on","js":"on"}}' | jq -r '.success' && echo -e "${GREEN}✅ Minification enabled${NC}" || echo -e "${YELLOW}⚠️ Could not enable minification${NC}"
|
||||
|
||||
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/brotli" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"value":"on"}' | jq -r '.success' && echo -e "${GREEN}✅ Brotli compression enabled${NC}" || echo -e "${YELLOW}⚠️ Could not enable Brotli${NC}"
|
||||
echo ""
|
||||
|
||||
# Add custom domain to Azure Static Web App
|
||||
echo -e "${GREEN}🔗 Adding Custom Domain to Azure Static Web App...${NC}"
|
||||
az staticwebapp hostname set \
|
||||
--name "$STATIC_WEB_APP_NAME" \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--hostname "$DOMAIN" 2>/dev/null && echo -e "${GREEN}✅ Custom domain $DOMAIN added${NC}" || echo -e "${YELLOW}⚠️ Domain may already be added or DNS not ready${NC}"
|
||||
|
||||
az staticwebapp hostname set \
|
||||
--name "$STATIC_WEB_APP_NAME" \
|
||||
--resource-group "$AZURE_RESOURCE_GROUP" \
|
||||
--hostname "www.$DOMAIN" 2>/dev/null && echo -e "${GREEN}✅ Custom domain www.$DOMAIN added${NC}" || echo -e "${YELLOW}⚠️ Domain may already be added or DNS not ready${NC}"
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo -e "${GREEN}✅ Cloudflare Setup Complete!${NC}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo "1. Verify DNS propagation (may take 24-48 hours)"
|
||||
echo "2. Verify SSL certificates are provisioned"
|
||||
echo "3. Test the website at https://$DOMAIN"
|
||||
echo "4. Monitor Cloudflare analytics"
|
||||
echo ""
|
||||
echo "DNS Records Created:"
|
||||
echo " - www.$DOMAIN -> $AZURE_STATIC_WEB_APP_URL"
|
||||
echo " - $DOMAIN -> $AZURE_STATIC_WEB_APP_URL"
|
||||
echo ""
|
||||
echo "Cloudflare Settings:"
|
||||
echo " - SSL Mode: Full (strict)"
|
||||
echo " - Always Use HTTPS: Enabled"
|
||||
echo " - Security Level: Medium"
|
||||
echo " - Minification: Enabled"
|
||||
echo " - Brotli Compression: Enabled"
|
||||
echo ""
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
# Script to store secrets in Azure Key Vault
|
||||
# This script reads from .env.production and stores secrets in Key Vault
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ResourceGroupName = "rg-miraclesinmotion-prod",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$KeyVaultName = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$CreateKeyVault = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "🔐 Storing Secrets in Azure Key Vault" -ForegroundColor Green
|
||||
Write-Host "====================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check if logged in to Azure
|
||||
$account = az account show --output json 2>$null | ConvertFrom-Json
|
||||
if (-not $account) {
|
||||
Write-Host "❌ Not logged in to Azure. Please run: az login" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Logged in to Azure" -ForegroundColor Green
|
||||
Write-Host " Subscription: $($account.name)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# Get Key Vault name
|
||||
if ([string]::IsNullOrEmpty($KeyVaultName)) {
|
||||
$KeyVaultName = az keyvault list --resource-group $ResourceGroupName --output json 2>$null | ConvertFrom-Json | Select-Object -First 1 -ExpandProperty name
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrEmpty($KeyVaultName)) {
|
||||
if ($CreateKeyVault) {
|
||||
$KeyVaultName = "mim-prod-$(Get-Random -Minimum 1000 -Maximum 9999)-kv"
|
||||
Write-Host "📦 Creating Key Vault: $KeyVaultName" -ForegroundColor Cyan
|
||||
|
||||
az keyvault create `
|
||||
--name $KeyVaultName `
|
||||
--resource-group $ResourceGroupName `
|
||||
--location $(az group show --name $ResourceGroupName --query location -o tsv) `
|
||||
--sku standard `
|
||||
--enable-rbac-authorization true `
|
||||
--enable-soft-delete true `
|
||||
--retention-days 90 | Out-Null
|
||||
|
||||
Write-Host "✅ Key Vault created" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Key Vault not found. Run with -CreateKeyVault to create one." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "✅ Found Key Vault: $KeyVaultName" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Check if .env.production exists
|
||||
$envFile = ".env.production"
|
||||
if (-not (Test-Path $envFile)) {
|
||||
Write-Host "❌ .env.production file not found. Please create it first." -ForegroundColor Red
|
||||
Write-Host " Run: .\scripts\populate-env.ps1" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "📄 Reading secrets from $envFile..." -ForegroundColor Cyan
|
||||
|
||||
# Read .env file and parse key-value pairs
|
||||
$envContent = Get-Content $envFile -Raw
|
||||
$secrets = @{}
|
||||
|
||||
# Parse environment variables (simple parser - handles KEY=VALUE format)
|
||||
$lines = $envContent -split "`n"
|
||||
foreach ($line in $lines) {
|
||||
$line = $line.Trim()
|
||||
if ($line -and -not $line.StartsWith("#") -and $line -match "^([^=]+)=(.*)$") {
|
||||
$key = $matches[1].Trim()
|
||||
$value = $matches[2].Trim()
|
||||
|
||||
# Skip empty values and placeholders
|
||||
if ($value -and $value -notmatch "^(your-|YOUR_|placeholder)" -and $value -ne "") {
|
||||
$secrets[$key] = $value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "✅ Found $($secrets.Count) secrets to store" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Define which secrets to store in Key Vault
|
||||
$secretsToStore = @(
|
||||
@{Name="azure-tenant-id"; EnvKey="AZURE_TENANT_ID"; Required=$true},
|
||||
@{Name="azure-client-id"; EnvKey="AZURE_CLIENT_ID"; Required=$false},
|
||||
@{Name="azure-client-secret"; EnvKey="AZURE_CLIENT_SECRET"; Required=$false},
|
||||
@{Name="stripe-publishable-key"; EnvKey="VITE_STRIPE_PUBLISHABLE_KEY"; Required=$false},
|
||||
@{Name="stripe-secret-key"; EnvKey="STRIPE_SECRET_KEY"; Required=$false},
|
||||
@{Name="stripe-webhook-secret"; EnvKey="STRIPE_WEBHOOK_SECRET"; Required=$false},
|
||||
@{Name="cosmos-endpoint"; EnvKey="COSMOS_ENDPOINT"; Required=$false},
|
||||
@{Name="cosmos-key"; EnvKey="COSMOS_KEY"; Required=$false},
|
||||
@{Name="cosmos-database-name"; EnvKey="COSMOS_DATABASE_NAME"; Required=$false},
|
||||
@{Name="app-insights-connection-string"; EnvKey="APPLICATIONINSIGHTS_CONNECTION_STRING"; Required=$false},
|
||||
@{Name="signalr-connection-string"; EnvKey="SIGNALR_CONNECTION_STRING"; Required=$false},
|
||||
@{Name="cloudflare-zone-id"; EnvKey="CLOUDFLARE_ZONE_ID"; Required=$false},
|
||||
@{Name="cloudflare-api-token"; EnvKey="CLOUDFLARE_API_TOKEN"; Required=$false},
|
||||
@{Name="salesforce-client-id"; EnvKey="SALESFORCE_CLIENT_ID"; Required=$false},
|
||||
@{Name="salesforce-client-secret"; EnvKey="SALESFORCE_CLIENT_SECRET"; Required=$false},
|
||||
@{Name="smtp-password"; EnvKey="SMTP_PASSWORD"; Required=$false},
|
||||
@{Name="session-secret"; EnvKey="SESSION_SECRET"; Required=$false},
|
||||
@{Name="jwt-secret"; EnvKey="JWT_SECRET"; Required=$false},
|
||||
@{Name="encryption-key"; EnvKey="ENCRYPTION_KEY"; Required=$false}
|
||||
)
|
||||
|
||||
$storedCount = 0
|
||||
$skippedCount = 0
|
||||
$errorCount = 0
|
||||
|
||||
Write-Host "📦 Storing secrets in Key Vault..." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
foreach ($secretDef in $secretsToStore) {
|
||||
$secretName = $secretDef.Name
|
||||
$envKey = $secretDef.EnvKey
|
||||
$required = $secretDef.Required
|
||||
|
||||
if ($secrets.ContainsKey($envKey)) {
|
||||
$secretValue = $secrets[$envKey]
|
||||
|
||||
try {
|
||||
Write-Host " Storing: $secretName" -ForegroundColor Gray
|
||||
az keyvault secret set `
|
||||
--vault-name $KeyVaultName `
|
||||
--name $secretName `
|
||||
--value $secretValue 2>$null | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host " ✅ Stored successfully" -ForegroundColor Green
|
||||
$storedCount++
|
||||
} else {
|
||||
Write-Host " ⚠️ Failed to store (may need RBAC permissions)" -ForegroundColor Yellow
|
||||
$errorCount++
|
||||
}
|
||||
} catch {
|
||||
Write-Host " ❌ Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||
$errorCount++
|
||||
}
|
||||
} else {
|
||||
if ($required) {
|
||||
Write-Host " ⚠️ Missing required secret: $secretName ($envKey)" -ForegroundColor Yellow
|
||||
$skippedCount++
|
||||
} else {
|
||||
Write-Host " ⏭️ Skipping: $secretName (not in .env file)" -ForegroundColor Gray
|
||||
$skippedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "📊 Summary:" -ForegroundColor Cyan
|
||||
Write-Host " ✅ Stored: $storedCount" -ForegroundColor Green
|
||||
Write-Host " ⏭️ Skipped: $skippedCount" -ForegroundColor Yellow
|
||||
Write-Host " ❌ Errors: $errorCount" -ForegroundColor $(if ($errorCount -gt 0) { "Red" } else { "Green" })
|
||||
Write-Host ""
|
||||
|
||||
# Prompt for additional secrets
|
||||
Write-Host "💡 Additional Secrets" -ForegroundColor Cyan
|
||||
Write-Host "You can manually add more secrets using:" -ForegroundColor Gray
|
||||
Write-Host " az keyvault secret set --vault-name $KeyVaultName --name <secret-name> --value <secret-value>" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
# Show how to retrieve secrets
|
||||
Write-Host "📖 Retrieving Secrets" -ForegroundColor Cyan
|
||||
Write-Host "To retrieve a secret:" -ForegroundColor Gray
|
||||
Write-Host " az keyvault secret show --vault-name $KeyVaultName --name <secret-name> --query value -o tsv" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
# Show Key Vault URL
|
||||
$keyVaultUrl = "https://$KeyVaultName.vault.azure.net/"
|
||||
Write-Host "🔗 Key Vault URL: $keyVaultUrl" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
if ($errorCount -gt 0) {
|
||||
Write-Host "⚠️ Some secrets failed to store. You may need to:" -ForegroundColor Yellow
|
||||
Write-Host "1. Grant yourself 'Key Vault Secrets Officer' role on the Key Vault" -ForegroundColor White
|
||||
Write-Host "2. Or use Azure Portal to manually add secrets" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Grant role command:" -ForegroundColor Cyan
|
||||
$currentUser = az ad signed-in-user show --query id -o tsv
|
||||
Write-Host " az role assignment create --role 'Key Vault Secrets Officer' --assignee $currentUser --scope /subscriptions/$($account.id)/resourceGroups/$ResourceGroupName/providers/Microsoft.KeyVault/vaults/$KeyVaultName" -ForegroundColor White
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "✅ Done!" -ForegroundColor Green
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Deployment Testing Script for Miracles in Motion
|
||||
# Tests all endpoints and verifies deployment status
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
STATIC_WEB_APP_URL="https://lemon-water-015cb3010.3.azurestaticapps.net"
|
||||
FUNCTION_APP_URL="https://mim-prod-igiay4-func.azurewebsites.net"
|
||||
RESOURCE_GROUP="rg-miraclesinmotion-prod"
|
||||
|
||||
echo -e "${GREEN}🧪 Starting Deployment Tests${NC}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Test counter
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Function to test endpoint
|
||||
test_endpoint() {
|
||||
local url=$1
|
||||
local name=$2
|
||||
local expected_code=${3:-200}
|
||||
|
||||
echo -n "Testing $name... "
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url" || echo "000")
|
||||
|
||||
if [ "$HTTP_CODE" = "$expected_code" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC} (HTTP $HTTP_CODE)"
|
||||
((TESTS_PASSED++))
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC} (HTTP $HTTP_CODE, expected $expected_code)"
|
||||
((TESTS_FAILED++))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test Static Web App
|
||||
echo -e "${YELLOW}📱 Testing Static Web App${NC}"
|
||||
test_endpoint "$STATIC_WEB_APP_URL" "Static Web App Homepage"
|
||||
test_endpoint "$STATIC_WEB_APP_URL/index.html" "Static Web App Index"
|
||||
test_endpoint "$STATIC_WEB_APP_URL/manifest.webmanifest" "PWA Manifest" 200
|
||||
|
||||
# Test Function App
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚡ Testing Function App${NC}"
|
||||
test_endpoint "$FUNCTION_APP_URL" "Function App Homepage"
|
||||
test_endpoint "$FUNCTION_APP_URL/api/health" "Function App Health Check" 200
|
||||
|
||||
# Test Azure Resources
|
||||
echo ""
|
||||
echo -e "${YELLOW}☁️ Testing Azure Resources${NC}"
|
||||
|
||||
# Check Static Web App status
|
||||
echo -n "Checking Static Web App status... "
|
||||
SWA_STATUS=$(az staticwebapp show --name mim-prod-igiay4-web --resource-group $RESOURCE_GROUP --query "properties.provisioningState" -o tsv 2>/dev/null || echo "Unknown")
|
||||
if [ "$SWA_STATUS" = "Succeeded" ] || [ "$SWA_STATUS" = "Ready" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC} (Status: $SWA_STATUS)"
|
||||
((TESTS_PASSED++))
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ WARNING${NC} (Status: $SWA_STATUS)"
|
||||
fi
|
||||
|
||||
# Check Function App status
|
||||
echo -n "Checking Function App status... "
|
||||
FA_STATUS=$(az functionapp show --name mim-prod-igiay4-func --resource-group $RESOURCE_GROUP --query "state" -o tsv 2>/dev/null || echo "Unknown")
|
||||
if [ "$FA_STATUS" = "Running" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC} (Status: $FA_STATUS)"
|
||||
((TESTS_PASSED++))
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC} (Status: $FA_STATUS)"
|
||||
((TESTS_FAILED++))
|
||||
fi
|
||||
|
||||
# Check Key Vault
|
||||
echo -n "Checking Key Vault... "
|
||||
KV_EXISTS=$(az keyvault show --name mim-prod-igiay4-kv --resource-group $RESOURCE_GROUP --query "name" -o tsv 2>/dev/null || echo "")
|
||||
if [ -n "$KV_EXISTS" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}"
|
||||
((TESTS_PASSED++))
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}"
|
||||
((TESTS_FAILED++))
|
||||
fi
|
||||
|
||||
# Check Application Insights
|
||||
echo -n "Checking Application Insights... "
|
||||
AI_EXISTS=$(az monitor app-insights component show --app mim-prod-igiay4-appinsights --resource-group $RESOURCE_GROUP --query "name" -o tsv 2>/dev/null || echo "")
|
||||
if [ -n "$AI_EXISTS" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}"
|
||||
((TESTS_PASSED++))
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}"
|
||||
((TESTS_FAILED++))
|
||||
fi
|
||||
|
||||
# Test SSL/TLS
|
||||
echo ""
|
||||
echo -e "${YELLOW}🔒 Testing SSL/TLS${NC}"
|
||||
echo -n "Testing HTTPS on Static Web App... "
|
||||
if echo | openssl s_client -connect lemon-water-015cb3010.3.azurestaticapps.net:443 -servername lemon-water-015cb3010.3.azurestaticapps.net 2>/dev/null | grep -q "Verify return code: 0"; then
|
||||
echo -e "${GREEN}✅ PASS${NC}"
|
||||
((TESTS_PASSED++))
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ WARNING${NC} (Could not verify SSL)"
|
||||
fi
|
||||
|
||||
# Test Performance
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚡ Testing Performance${NC}"
|
||||
echo -n "Testing Static Web App response time... "
|
||||
RESPONSE_TIME=$(curl -s -o /dev/null -w "%{time_total}" --max-time 10 "$STATIC_WEB_APP_URL" || echo "999")
|
||||
if (( $(echo "$RESPONSE_TIME < 3.0" | bc -l) )); then
|
||||
echo -e "${GREEN}✅ PASS${NC} (${RESPONSE_TIME}s)"
|
||||
((TESTS_PASSED++))
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ WARNING${NC} (${RESPONSE_TIME}s - may be slow)"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo -e "${GREEN}Tests Passed: $TESTS_PASSED${NC}"
|
||||
if [ $TESTS_FAILED -gt 0 ]; then
|
||||
echo -e "${RED}Tests Failed: $TESTS_FAILED${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}Tests Failed: $TESTS_FAILED${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ All tests passed!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"route": "/api/*",
|
||||
"allowedRoles": ["anonymous", "authenticated"]
|
||||
},
|
||||
{
|
||||
"route": "/admin/*",
|
||||
"allowedRoles": ["Admin"]
|
||||
},
|
||||
{
|
||||
"route": "/volunteer/*",
|
||||
"allowedRoles": ["Volunteer", "Admin"]
|
||||
},
|
||||
{
|
||||
"route": "/resource/*",
|
||||
"allowedRoles": ["Resource", "Admin"]
|
||||
},
|
||||
{
|
||||
"route": "/portals/*",
|
||||
"allowedRoles": ["authenticated"]
|
||||
},
|
||||
{
|
||||
"route": "/analytics/*",
|
||||
"allowedRoles": ["Admin"]
|
||||
},
|
||||
{
|
||||
"route": "/ai-portal/*",
|
||||
"allowedRoles": ["Admin", "Volunteer"]
|
||||
},
|
||||
{
|
||||
"route": "/*",
|
||||
"rewrite": "/index.html"
|
||||
}
|
||||
],
|
||||
"navigationFallback": {
|
||||
"rewrite": "/index.html",
|
||||
"exclude": ["/api/*", "/admin/*", "/volunteer/*", "/resource/*"]
|
||||
},
|
||||
"responseOverrides": {
|
||||
"401": {
|
||||
"redirect": "/#/portals",
|
||||
"statusCode": 302
|
||||
},
|
||||
"403": {
|
||||
"redirect": "/#/portals",
|
||||
"statusCode": 302
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"identityProviders": {
|
||||
"azureActiveDirectory": {
|
||||
"registration": {
|
||||
"openIdIssuer": "https://login.microsoftonline.com/{tenantId}/v2.0",
|
||||
"clientIdSettingName": "AZURE_CLIENT_ID",
|
||||
"clientSecretSettingName": "AZURE_CLIENT_SECRET"
|
||||
},
|
||||
"userDetailsClaim": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
||||
"userDetailsPrincipalName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"globalHeaders": {
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"Permissions-Policy": "geolocation=(), microphone=(), camera=()",
|
||||
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||
"Content-Security-Policy": "default-src 'self' https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https: blob:; font-src 'self' https: data:; connect-src 'self' https: wss:; media-src 'self' https: data:; object-src 'none'; base-uri 'self'; form-action 'self' https:; frame-ancestors 'none'; upgrade-insecure-requests"
|
||||
},
|
||||
"mimeTypes": {
|
||||
".json": "application/json",
|
||||
".js": "text/javascript",
|
||||
".css": "text/css",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".ico": "image/x-icon",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
".eot": "application/vnd.ms-fontobject"
|
||||
},
|
||||
"platform": {
|
||||
"apiRuntime": "node:22"
|
||||
},
|
||||
"forwardingGateway": {
|
||||
"allowedForwardedHosts": [
|
||||
"mim4u.org",
|
||||
"www.mim4u.org"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
|
||||
"configurations": {
|
||||
"miracles-in-motion": {
|
||||
"appLocation": ".",
|
||||
"apiLocation": "api",
|
||||
"outputLocation": "dist",
|
||||
"apiLanguage": "node",
|
||||
"apiVersion": "20",
|
||||
"appBuildCommand": "npm run build",
|
||||
"apiBuildCommand": "npm run build --if-present",
|
||||
"run": "npm run dev",
|
||||
"appDevserverUrl": "http://localhost:5173"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user