Deploy to production - ensure all endpoints operational

This commit is contained in:
defiQUG
2025-11-12 08:17:28 -08:00
parent b421d2964c
commit f1c61c8339
171 changed files with 50830 additions and 42363 deletions

View File

@@ -0,0 +1,350 @@
# 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
}

View File

@@ -1,30 +1,30 @@
#!/usr/bin/env node
/**
* Export Mermaid architecture diagram to PNG/SVG.
* Requires: @mermaid-js/mermaid-cli (mmdc)
* Usage:
* node scripts/export-architecture.mjs --format png
* node scripts/export-architecture.mjs --format svg
*/
import { execSync } from 'child_process'
import { existsSync } from 'fs'
import { join } from 'path'
const args = process.argv.slice(2)
const formatArg = args.find(a => a.startsWith('--format=')) || '--format=png'
const format = formatArg.split('=')[1]
const diagram = join(process.cwd(), 'docs', 'ArchitectureDiagram.mmd')
const outFile = join(process.cwd(), 'docs', `ArchitectureDiagram.${format}`)
if (!existsSync(diagram)) {
console.error('Diagram source not found:', diagram)
process.exit(1)
}
try {
execSync(`npx mmdc -i "${diagram}" -o "${outFile}"`, { stdio: 'inherit' })
console.log(`Exported diagram to ${outFile}`)
} catch (e) {
console.error('Mermaid export failed. Ensure @mermaid-js/mermaid-cli is installed.')
process.exit(1)
#!/usr/bin/env node
/**
* Export Mermaid architecture diagram to PNG/SVG.
* Requires: @mermaid-js/mermaid-cli (mmdc)
* Usage:
* node scripts/export-architecture.mjs --format png
* node scripts/export-architecture.mjs --format svg
*/
import { execSync } from 'child_process'
import { existsSync } from 'fs'
import { join } from 'path'
const args = process.argv.slice(2)
const formatArg = args.find(a => a.startsWith('--format=')) || '--format=png'
const format = formatArg.split('=')[1]
const diagram = join(process.cwd(), 'docs', 'ArchitectureDiagram.mmd')
const outFile = join(process.cwd(), 'docs', `ArchitectureDiagram.${format}`)
if (!existsSync(diagram)) {
console.error('Diagram source not found:', diagram)
process.exit(1)
}
try {
execSync(`npx mmdc -i "${diagram}" -o "${outFile}"`, { stdio: 'inherit' })
console.log(`Exported diagram to ${outFile}`)
} catch (e) {
console.error('Mermaid export failed. Ensure @mermaid-js/mermaid-cli is installed.')
process.exit(1)
}

View File

@@ -1,65 +1,65 @@
#!/usr/bin/env node
/**
* Auto-generate docs/README.md index.
* Scans docs directory for .md files (excluding README.md) and categorizes by simple heuristics.
*/
import { readdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const DOCS_DIR = join(process.cwd(), 'docs')
const OUTPUT = join(DOCS_DIR, 'README.md')
// Basic categorization keywords
const categories = [
{ name: 'Getting Started', match: [/quickstart/i, /usermanual/i] },
{ name: 'Architecture & Engineering', match: [/architecture/i, /implementation/i, /diagram/i] },
{ name: 'Delivery & Reports', match: [/completion/i, /deployment/, /update/, /phases_all/i, /production_deployment/ ] },
{ name: 'Performance & Optimization', match: [/performance/i, /seo/i] },
{ name: 'Change History', match: [/changelog/i] },
{ name: 'AI & Advanced Features', match: [/ai/i] }
]
function categorize(file) {
const lower = file.toLowerCase()
for (const cat of categories) {
if (cat.match.some(r => r.test(lower))) return cat.name
}
return 'Other'
}
function build() {
const files = readdirSync(DOCS_DIR)
.filter(f => f.endsWith('.md') && f !== 'README.md')
.sort()
const byCategory = {}
for (const f of files) {
const cat = categorize(f)
byCategory[cat] = byCategory[cat] || []
byCategory[cat].push(f)
}
const quickLinks = files.map(f => `- [${f.replace(/_/g,' ')}](./${f})`).join('\n')
let body = '# Documentation Index\n\n(Generated by scripts/generate-doc-index.mjs)\n\n## Quick Links\n' + quickLinks + '\n\n'
for (const cat of Object.keys(byCategory).sort()) {
body += `### ${cat}\n` + byCategory[cat].map(f => `- ${f}`).join('\n') + '\n\n'
}
// Append diagram export instructions (persistent section)
body += '## Diagram Export\n'
body += 'The architecture diagram source is `ArchitectureDiagram.mmd`. Export updated images using:\n\n'
body += '```bash\n'
body += 'npm run diagram:png\n'
body += 'npm run diagram:svg\n'
body += '```\n\n'
body += 'Refresh docs index and PNG in one step:\n\n'
body += '```bash\n'
body += 'npm run docs:refresh\n'
body += '```\n\n'
body += '---\nLast regenerated: ' + new Date().toISOString() + '\n'
writeFileSync(OUTPUT, body)
console.log('docs/README.md regenerated.')
}
#!/usr/bin/env node
/**
* Auto-generate docs/README.md index.
* Scans docs directory for .md files (excluding README.md) and categorizes by simple heuristics.
*/
import { readdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const DOCS_DIR = join(process.cwd(), 'docs')
const OUTPUT = join(DOCS_DIR, 'README.md')
// Basic categorization keywords
const categories = [
{ name: 'Getting Started', match: [/quickstart/i, /usermanual/i] },
{ name: 'Architecture & Engineering', match: [/architecture/i, /implementation/i, /diagram/i] },
{ name: 'Delivery & Reports', match: [/completion/i, /deployment/, /update/, /phases_all/i, /production_deployment/ ] },
{ name: 'Performance & Optimization', match: [/performance/i, /seo/i] },
{ name: 'Change History', match: [/changelog/i] },
{ name: 'AI & Advanced Features', match: [/ai/i] }
]
function categorize(file) {
const lower = file.toLowerCase()
for (const cat of categories) {
if (cat.match.some(r => r.test(lower))) return cat.name
}
return 'Other'
}
function build() {
const files = readdirSync(DOCS_DIR)
.filter(f => f.endsWith('.md') && f !== 'README.md')
.sort()
const byCategory = {}
for (const f of files) {
const cat = categorize(f)
byCategory[cat] = byCategory[cat] || []
byCategory[cat].push(f)
}
const quickLinks = files.map(f => `- [${f.replace(/_/g,' ')}](./${f})`).join('\n')
let body = '# Documentation Index\n\n(Generated by scripts/generate-doc-index.mjs)\n\n## Quick Links\n' + quickLinks + '\n\n'
for (const cat of Object.keys(byCategory).sort()) {
body += `### ${cat}\n` + byCategory[cat].map(f => `- ${f}`).join('\n') + '\n\n'
}
// Append diagram export instructions (persistent section)
body += '## Diagram Export\n'
body += 'The architecture diagram source is `ArchitectureDiagram.mmd`. Export updated images using:\n\n'
body += '```bash\n'
body += 'npm run diagram:png\n'
body += 'npm run diagram:svg\n'
body += '```\n\n'
body += 'Refresh docs index and PNG in one step:\n\n'
body += '```bash\n'
body += 'npm run docs:refresh\n'
body += '```\n\n'
body += '---\nLast regenerated: ' + new Date().toISOString() + '\n'
writeFileSync(OUTPUT, body)
console.log('docs/README.md regenerated.')
}
build()

View File

@@ -1,129 +1,129 @@
#!/usr/bin/env node
/**
* Generate Open Graph images for Miracles In Motion
* This script creates social media preview images
*/
import fs from 'fs'
import path from 'path'
const OG_CONFIG = {
width: 1200,
height: 630,
title: 'Miracles In Motion',
subtitle: 'Essentials for Every Student',
description: '501(c)3 Non-Profit Organization'
}
/**
* Create SVG template for OG image
*/
function createOGImageSVG(config = OG_CONFIG) {
return `
<svg width="${config.width}" height="${config.height}" viewBox="0 0 ${config.width} ${config.height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ec4899;stop-opacity:1" />
<stop offset="50%" style="stop-color:#8b5cf6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="rgba(0,0,0,0.25)"/>
</filter>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bg-gradient)"/>
<!-- Pattern overlay -->
<pattern id="dots" patternUnits="userSpaceOnUse" width="40" height="40">
<circle cx="20" cy="20" r="2" fill="rgba(255,255,255,0.1)"/>
</pattern>
<rect width="100%" height="100%" fill="url(#dots)"/>
<!-- Content container -->
<g transform="translate(80, 0)">
<!-- Logo area -->
<rect x="0" y="120" width="80" height="80" rx="20" fill="rgba(255,255,255,0.2)" filter="url(#shadow)"/>
<circle cx="40" cy="160" r="20" fill="white"/>
<!-- Text content -->
<text x="120" y="140" font-family="system-ui, -apple-system, sans-serif" font-size="48" font-weight="700" fill="white">
${config.title}
</text>
<text x="120" y="180" font-family="system-ui, -apple-system, sans-serif" font-size="28" font-weight="400" fill="rgba(255,255,255,0.9)">
${config.subtitle}
</text>
<text x="120" y="220" font-family="system-ui, -apple-system, sans-serif" font-size="20" font-weight="300" fill="rgba(255,255,255,0.8)">
${config.description}
</text>
<!-- Call to action -->
<rect x="120" y="280" width="200" height="50" rx="25" fill="rgba(255,255,255,0.2)" filter="url(#shadow)"/>
<text x="220" y="310" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="500" fill="white" text-anchor="middle">
Learn More
</text>
</g>
<!-- Bottom accent -->
<rect x="0" y="580" width="100%" height="50" fill="rgba(0,0,0,0.1)"/>
<text x="600" y="610" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="rgba(255,255,255,0.8)" text-anchor="middle">
miraclesinmotion.org
</text>
</svg>
`.trim()
}
/**
* Generate OG images
*/
function generateOGImages() {
const publicDir = path.join(process.cwd(), 'public')
// Ensure public directory exists
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true })
}
// Create default OG image
const defaultOG = createOGImageSVG()
fs.writeFileSync(path.join(publicDir, 'og-image.svg'), defaultOG)
console.log('✅ Generated og-image.svg')
// Create page-specific OG images
const pages = [
{ name: 'donate', title: 'Donate', subtitle: 'Help Students Succeed' },
{ name: 'volunteer', title: 'Volunteer', subtitle: 'Make a Difference' },
{ name: 'stories', title: 'Stories', subtitle: 'Impact in Action' },
]
pages.forEach(page => {
const pageOG = createOGImageSVG({
...OG_CONFIG,
title: page.title,
subtitle: page.subtitle
})
fs.writeFileSync(path.join(publicDir, `og-image-${page.name}.svg`), pageOG)
console.log(`✅ Generated og-image-${page.name}.svg`)
})
console.log('\n🎉 All OG images generated successfully!')
console.log('\nNote: These are SVG files. For production, consider converting to PNG using a tool like:')
console.log('- Puppeteer for programmatic conversion')
console.log('- Online converters')
console.log('- Design tools like Figma or Canva')
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
try {
generateOGImages()
} catch (error) {
console.error('❌ Error generating OG images:', error)
process.exit(1)
}
}
#!/usr/bin/env node
/**
* Generate Open Graph images for Miracles In Motion
* This script creates social media preview images
*/
import fs from 'fs'
import path from 'path'
const OG_CONFIG = {
width: 1200,
height: 630,
title: 'Miracles In Motion',
subtitle: 'Essentials for Every Student',
description: '501(c)3 Non-Profit Organization'
}
/**
* Create SVG template for OG image
*/
function createOGImageSVG(config = OG_CONFIG) {
return `
<svg width="${config.width}" height="${config.height}" viewBox="0 0 ${config.width} ${config.height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ec4899;stop-opacity:1" />
<stop offset="50%" style="stop-color:#8b5cf6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="rgba(0,0,0,0.25)"/>
</filter>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bg-gradient)"/>
<!-- Pattern overlay -->
<pattern id="dots" patternUnits="userSpaceOnUse" width="40" height="40">
<circle cx="20" cy="20" r="2" fill="rgba(255,255,255,0.1)"/>
</pattern>
<rect width="100%" height="100%" fill="url(#dots)"/>
<!-- Content container -->
<g transform="translate(80, 0)">
<!-- Logo area -->
<rect x="0" y="120" width="80" height="80" rx="20" fill="rgba(255,255,255,0.2)" filter="url(#shadow)"/>
<circle cx="40" cy="160" r="20" fill="white"/>
<!-- Text content -->
<text x="120" y="140" font-family="system-ui, -apple-system, sans-serif" font-size="48" font-weight="700" fill="white">
${config.title}
</text>
<text x="120" y="180" font-family="system-ui, -apple-system, sans-serif" font-size="28" font-weight="400" fill="rgba(255,255,255,0.9)">
${config.subtitle}
</text>
<text x="120" y="220" font-family="system-ui, -apple-system, sans-serif" font-size="20" font-weight="300" fill="rgba(255,255,255,0.8)">
${config.description}
</text>
<!-- Call to action -->
<rect x="120" y="280" width="200" height="50" rx="25" fill="rgba(255,255,255,0.2)" filter="url(#shadow)"/>
<text x="220" y="310" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="500" fill="white" text-anchor="middle">
Learn More
</text>
</g>
<!-- Bottom accent -->
<rect x="0" y="580" width="100%" height="50" fill="rgba(0,0,0,0.1)"/>
<text x="600" y="610" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="rgba(255,255,255,0.8)" text-anchor="middle">
miraclesinmotion.org
</text>
</svg>
`.trim()
}
/**
* Generate OG images
*/
function generateOGImages() {
const publicDir = path.join(process.cwd(), 'public')
// Ensure public directory exists
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true })
}
// Create default OG image
const defaultOG = createOGImageSVG()
fs.writeFileSync(path.join(publicDir, 'og-image.svg'), defaultOG)
console.log('✅ Generated og-image.svg')
// Create page-specific OG images
const pages = [
{ name: 'donate', title: 'Donate', subtitle: 'Help Students Succeed' },
{ name: 'volunteer', title: 'Volunteer', subtitle: 'Make a Difference' },
{ name: 'stories', title: 'Stories', subtitle: 'Impact in Action' },
]
pages.forEach(page => {
const pageOG = createOGImageSVG({
...OG_CONFIG,
title: page.title,
subtitle: page.subtitle
})
fs.writeFileSync(path.join(publicDir, `og-image-${page.name}.svg`), pageOG)
console.log(`✅ Generated og-image-${page.name}.svg`)
})
console.log('\n🎉 All OG images generated successfully!')
console.log('\nNote: These are SVG files. For production, consider converting to PNG using a tool like:')
console.log('- Puppeteer for programmatic conversion')
console.log('- Online converters')
console.log('- Design tools like Figma or Canva')
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
try {
generateOGImages()
} catch (error) {
console.error('❌ Error generating OG images:', error)
process.exit(1)
}
}
export { generateOGImages, createOGImageSVG }

273
scripts/populate-env.ps1 Normal file
View File

@@ -0,0 +1,273 @@
# 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 ""

View File

@@ -0,0 +1,272 @@
# 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 ""

View File

@@ -0,0 +1,295 @@
#!/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 ""

View File

@@ -0,0 +1,298 @@
#!/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 ""

View File

@@ -0,0 +1,290 @@
# 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
scripts/setup-cloudflare.sh Normal file
View File

@@ -0,0 +1,240 @@
#!/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 ""

View File

@@ -0,0 +1,197 @@
# 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
scripts/test-deployment.sh Normal file
View File

@@ -0,0 +1,141 @@
#!/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