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:
defiQUG
2026-06-15 19:41:08 -07:00
co-authored by Cursor
parent d5a8a263cb
commit e3a171c5ff
39 changed files with 168 additions and 10642 deletions
@@ -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)
-249
View File
@@ -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"
-5
View File
@@ -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/
BIN
View File
Binary file not shown.
-21
View File
@@ -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"]
}
}
}
-34
View File
@@ -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"
}
}
-21
View File
@@ -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"]
}
}
}
-4762
View File
File diff suppressed because it is too large Load Diff
-34
View File
@@ -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"
}
}
-174
View File
@@ -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;
-181
View File
@@ -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
});
-139
View File
@@ -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
});
-90
View File
@@ -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
});
-3
View File
@@ -1,3 +0,0 @@
import './donations/createDonation';
import './donations/getDonations';
import './ai/chat';
-180
View File
@@ -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;
}
-19
View File
@@ -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"]
}
+36 -747
View File
@@ -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
View File
@@ -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).
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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
-472
View File
@@ -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"
}
}
}
-323
View File
@@ -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}'
-18
View File
@@ -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"
}
}
}
-202
View File
@@ -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
-62
View File
@@ -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
-53
View File
@@ -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
-350
View File
@@ -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
}
-273
View File
@@ -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 ""
-272
View File
@@ -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 ""
-295
View File
@@ -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 ""
-298
View File
@@ -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 ""
-290
View File
@@ -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 ""
-240
View File
@@ -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 ""
-197
View File
@@ -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
-141
View File
@@ -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
-96
View File
@@ -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"
]
}
}
-16
View File
@@ -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"
}
}
}