Deploy to production - ensure all endpoints operational
This commit is contained in:
350
scripts/deployment-checklist.ps1
Normal file
350
scripts/deployment-checklist.ps1
Normal 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
273
scripts/populate-env.ps1
Normal 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 ""
|
||||
|
||||
272
scripts/setup-azure-entra.ps1
Normal file
272
scripts/setup-azure-entra.ps1
Normal 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 ""
|
||||
|
||||
295
scripts/setup-azure-entra.sh
Normal file
295
scripts/setup-azure-entra.sh
Normal 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 ""
|
||||
|
||||
298
scripts/setup-cloudflare-auto.sh
Normal file
298
scripts/setup-cloudflare-auto.sh
Normal 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 ""
|
||||
|
||||
290
scripts/setup-cloudflare.ps1
Normal file
290
scripts/setup-cloudflare.ps1
Normal 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
240
scripts/setup-cloudflare.sh
Normal 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 ""
|
||||
|
||||
197
scripts/store-secrets-in-keyvault.ps1
Normal file
197
scripts/store-secrets-in-keyvault.ps1
Normal 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
141
scripts/test-deployment.sh
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user