feat(api): initialize Azure Functions API for Miracles in Motion platform

- Added package.json with dependencies and scripts for building and testing the API.
- Implemented DIContainer for managing service instances (Cosmos DB, Key Vault).
- Created createDonation function to handle donation creation and Stripe payment processing.
- Implemented getDonations function for fetching donations with pagination and filtering.
- Defined types for Donation, Volunteer, Program, and API responses.
- Configured TypeScript with tsconfig.json for strict type checking and output settings.
- Developed deployment scripts for production and simple deployments to Azure.
- Created Bicep templates for infrastructure setup including Cosmos DB, Key Vault, and Function App.
- Added parameters for deployment configuration in main.parameters.json.
- Configured static web app settings in staticwebapp.config.json for routing and security.
This commit is contained in:
defiQUG
2025-10-05 14:33:52 -07:00
parent 37469c105c
commit ce821932ce
13 changed files with 5805 additions and 0 deletions

16
api/host.json Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
},
"functionTimeout": "00:05:00"
}

4760
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
api/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "miracles-in-motion-api",
"version": "1.0.0",
"description": "Azure Functions API for Miracles in Motion nonprofit platform",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"prestart": "npm run build",
"start": "func start",
"test": "jest"
},
"dependencies": {
"@azure/cosmos": "^4.0.0",
"@azure/keyvault-secrets": "^4.8.0",
"@azure/identity": "^4.0.1",
"@azure/functions": "^4.0.1",
"stripe": "^14.10.0",
"joi": "^17.12.0",
"uuid": "^9.0.1",
"cors": "^2.8.5"
},
"devDependencies": {
"@types/node": "^20.10.6",
"@types/uuid": "^9.0.7",
"@types/cors": "^2.8.17",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"@types/jest": "^29.5.11"
},
"engines": {
"node": ">=18.0.0"
}
}

82
api/src/DIContainer.ts Normal file
View File

@@ -0,0 +1,82 @@
import { CosmosClient, Database, Container } from '@azure/cosmos';
import { SecretClient } from '@azure/keyvault-secrets';
import { DefaultAzureCredential } from '@azure/identity';
export interface ServiceContainer {
cosmosClient: CosmosClient;
database: Database;
donationsContainer: Container;
volunteersContainer: Container;
programsContainer: Container;
secretClient: SecretClient;
}
class DIContainer {
private static instance: DIContainer;
private services: ServiceContainer | null = null;
private constructor() {}
public static getInstance(): DIContainer {
if (!DIContainer.instance) {
DIContainer.instance = new DIContainer();
}
return DIContainer.instance;
}
public async initializeServices(): Promise<ServiceContainer> {
if (this.services) {
return this.services;
}
try {
// Initialize Cosmos DB
const cosmosConnectionString = process.env.COSMOS_CONNECTION_STRING;
if (!cosmosConnectionString) {
throw new Error('COSMOS_CONNECTION_STRING is not configured');
}
const cosmosClient = new CosmosClient(cosmosConnectionString);
const databaseName = process.env.COSMOS_DATABASE_NAME || 'MiraclesInMotion';
const database = cosmosClient.database(databaseName);
// Get containers
const donationsContainer = database.container('donations');
const volunteersContainer = database.container('volunteers');
const programsContainer = database.container('programs');
// Initialize Key Vault
const keyVaultUrl = process.env.KEY_VAULT_URL;
if (!keyVaultUrl) {
throw new Error('KEY_VAULT_URL is not configured');
}
const credential = new DefaultAzureCredential();
const secretClient = new SecretClient(keyVaultUrl, credential);
this.services = {
cosmosClient,
database,
donationsContainer,
volunteersContainer,
programsContainer,
secretClient
};
console.log('✅ Services initialized successfully');
return this.services;
} catch (error) {
console.error('❌ Failed to initialize services:', error);
throw error;
}
}
public getServices(): ServiceContainer {
if (!this.services) {
throw new Error('Services not initialized. Call initializeServices() first.');
}
return this.services;
}
}
export default DIContainer;

View File

@@ -0,0 +1,135 @@
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import DIContainer from '../DIContainer';
import { ApiResponse, CreateDonationRequest, Donation } from '../types';
import { v4 as uuidv4 } from 'uuid';
import Stripe from 'stripe';
export async function createDonation(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
try {
await DIContainer.getInstance().initializeServices();
const { donationsContainer, secretClient } = DIContainer.getInstance().getServices();
// Get request body
const donationRequest = await request.json() as CreateDonationRequest;
// Validate required fields
if (!donationRequest.amount || !donationRequest.donorEmail || !donationRequest.donorName) {
const response: ApiResponse = {
success: false,
error: 'Missing required fields: amount, donorEmail, donorName',
timestamp: new Date().toISOString()
};
return {
status: 400,
jsonBody: response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
};
}
// Initialize Stripe if payment method is stripe
let stripePaymentIntentId: string | undefined;
if (donationRequest.paymentMethod === 'stripe') {
try {
const stripeSecretKey = await secretClient.getSecret('stripe-secret-key');
const stripe = new Stripe(stripeSecretKey.value!, {
apiVersion: '2023-10-16'
});
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(donationRequest.amount * 100), // Convert to cents
currency: donationRequest.currency.toLowerCase(),
metadata: {
donorEmail: donationRequest.donorEmail,
donorName: donationRequest.donorName,
program: donationRequest.program || 'general'
}
});
stripePaymentIntentId = paymentIntent.id;
} catch (stripeError) {
context.error('Stripe payment intent creation failed:', stripeError);
const response: ApiResponse = {
success: false,
error: 'Payment processing failed',
timestamp: new Date().toISOString()
};
return {
status: 500,
jsonBody: response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
};
}
}
// Create donation record
const donation: Donation = {
id: uuidv4(),
amount: donationRequest.amount,
currency: donationRequest.currency,
donorName: donationRequest.donorName,
donorEmail: donationRequest.donorEmail,
donorPhone: donationRequest.donorPhone,
program: donationRequest.program,
isRecurring: donationRequest.isRecurring,
frequency: donationRequest.frequency,
paymentMethod: donationRequest.paymentMethod,
stripePaymentIntentId,
status: 'pending',
message: donationRequest.message,
isAnonymous: donationRequest.isAnonymous,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// Save to Cosmos DB
await donationsContainer.items.create(donation);
const response: ApiResponse<Donation> = {
success: true,
data: donation,
message: 'Donation created successfully',
timestamp: new Date().toISOString()
};
return {
status: 201,
jsonBody: response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
};
} catch (error) {
context.error('Error creating donation:', error);
const response: ApiResponse = {
success: false,
error: 'Failed to create donation',
timestamp: new Date().toISOString()
};
return {
status: 500,
jsonBody: response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
};
}
}
app.http('createDonation', {
methods: ['POST'],
authLevel: 'anonymous',
route: 'donations',
handler: createDonation
});

View File

@@ -0,0 +1,90 @@
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import DIContainer from '../DIContainer';
import { ApiResponse, PaginatedResponse, Donation } from '../types';
import { v4 as uuidv4 } from 'uuid';
export async function getDonations(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
try {
await DIContainer.getInstance().initializeServices();
const { donationsContainer } = DIContainer.getInstance().getServices();
const page = parseInt(request.query.get('page') || '1');
const limit = parseInt(request.query.get('limit') || '10');
const status = request.query.get('status');
const program = request.query.get('program');
let query = 'SELECT * FROM c WHERE 1=1';
const parameters: any[] = [];
if (status) {
query += ' AND c.status = @status';
parameters.push({ name: '@status', value: status });
}
if (program) {
query += ' AND c.program = @program';
parameters.push({ name: '@program', value: program });
}
query += ' ORDER BY c.createdAt DESC';
const { resources: donations } = await donationsContainer.items
.query({
query,
parameters
})
.fetchAll();
// Simple pagination
const total = donations.length;
const pages = Math.ceil(total / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedDonations = donations.slice(startIndex, endIndex);
const response: PaginatedResponse<Donation> = {
success: true,
data: paginatedDonations,
pagination: {
page,
limit,
total,
pages
},
timestamp: new Date().toISOString()
};
return {
status: 200,
jsonBody: response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
};
} catch (error) {
context.error('Error fetching donations:', error);
const response: ApiResponse = {
success: false,
error: 'Failed to fetch donations',
timestamp: new Date().toISOString()
};
return {
status: 500,
jsonBody: response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
};
}
}
app.http('getDonations', {
methods: ['GET'],
authLevel: 'anonymous',
route: 'donations',
handler: getDonations
});

180
api/src/types.ts Normal file
View File

@@ -0,0 +1,180 @@
export interface Donation {
id: string;
amount: number;
currency: string;
donorName: string;
donorEmail: string;
donorPhone?: string;
program?: string;
isRecurring: boolean;
frequency?: 'monthly' | 'quarterly' | 'annually';
paymentMethod: 'stripe' | 'paypal' | 'bank_transfer';
stripePaymentIntentId?: string;
status: 'pending' | 'completed' | 'failed' | 'cancelled' | 'refunded';
message?: string;
isAnonymous: boolean;
createdAt: string;
updatedAt: string;
metadata?: Record<string, any>;
}
export interface Volunteer {
id: string;
firstName: string;
lastName: string;
email: string;
phone: string;
dateOfBirth: string;
address: {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
};
emergencyContact: {
name: string;
phone: string;
relationship: string;
};
skills: string[];
interests: string[];
availability: {
monday: boolean;
tuesday: boolean;
wednesday: boolean;
thursday: boolean;
friday: boolean;
saturday: boolean;
sunday: boolean;
timeSlots: string[];
};
experience: string;
motivation: string;
backgroundCheck: {
completed: boolean;
completedDate?: string;
status?: 'pending' | 'approved' | 'rejected';
};
status: 'pending' | 'approved' | 'inactive' | 'suspended';
createdAt: string;
updatedAt: string;
lastActivityAt?: string;
}
export interface Program {
id: string;
name: string;
description: string;
category: 'education' | 'healthcare' | 'community' | 'environment' | 'arts' | 'other';
targetAudience: string;
goals: string[];
location: {
type: 'physical' | 'virtual' | 'hybrid';
address?: string;
city?: string;
state?: string;
country?: string;
virtualLink?: string;
};
schedule: {
startDate: string;
endDate?: string;
frequency: 'one-time' | 'weekly' | 'monthly' | 'ongoing';
daysOfWeek: string[];
timeSlots: string[];
};
requirements: {
minimumAge?: number;
maximumAge?: number;
skills?: string[];
experience?: string;
other?: string[];
};
capacity: {
minimum: number;
maximum: number;
current: number;
};
budget: {
total: number;
raised: number;
currency: string;
};
coordinator: {
name: string;
email: string;
phone: string;
};
volunteers: string[]; // Array of volunteer IDs
status: 'planning' | 'active' | 'completed' | 'cancelled' | 'on-hold';
createdAt: string;
updatedAt: string;
images?: string[];
documents?: string[];
}
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
message?: string;
timestamp: string;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
}
export interface CreateDonationRequest {
amount: number;
currency: string;
donorName: string;
donorEmail: string;
donorPhone?: string;
program?: string;
isRecurring: boolean;
frequency?: 'monthly' | 'quarterly' | 'annually';
paymentMethod: 'stripe' | 'paypal' | 'bank_transfer';
message?: string;
isAnonymous: boolean;
}
export interface CreateVolunteerRequest {
firstName: string;
lastName: string;
email: string;
phone: string;
dateOfBirth: string;
address: {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
};
emergencyContact: {
name: string;
phone: string;
relationship: string;
};
skills: string[];
interests: string[];
availability: {
monday: boolean;
tuesday: boolean;
wednesday: boolean;
thursday: boolean;
friday: boolean;
saturday: boolean;
sunday: boolean;
timeSlots: string[];
};
experience: string;
motivation: string;
}

19
api/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts", "dist"]
}

62
deploy-production.ps1 Normal file
View File

@@ -0,0 +1,62 @@
# Miracles in Motion - Production Deployment Script
param(
[string]$ResourceGroupName = "rg-miraclesinmotion-prod",
[string]$Location = "East US 2",
[string]$SubscriptionId = "6187c4d0-3c1a-4135-a8b5-c9782fcf0743"
)
Write-Host "🚀 Starting Miracles in Motion Production Deployment" -ForegroundColor Green
# Set subscription
Write-Host "Setting Azure subscription..." -ForegroundColor Yellow
az account set --subscription $SubscriptionId
# Create resource group
Write-Host "Creating resource group: $ResourceGroupName" -ForegroundColor Yellow
az group create --name $ResourceGroupName --location $Location
# Deploy infrastructure using Bicep
Write-Host "Deploying Azure infrastructure..." -ForegroundColor Yellow
$deploymentResult = az deployment group create `
--resource-group $ResourceGroupName `
--template-file infrastructure/main.bicep `
--parameters @infrastructure/main.parameters.json `
--query 'properties.outputs' `
--output json
if ($LASTEXITCODE -ne 0) {
Write-Error "Infrastructure deployment failed!"
exit 1
}
Write-Host "✅ Infrastructure deployed successfully!" -ForegroundColor Green
# Parse deployment outputs
$outputs = $deploymentResult | ConvertFrom-Json
# Build and deploy Functions
Write-Host "Building Azure Functions..." -ForegroundColor Yellow
Set-Location api
npm install
npm run build
# Deploy Functions
Write-Host "Deploying Azure Functions..." -ForegroundColor Yellow
func azure functionapp publish $outputs.functionAppName.value
# Build frontend
Write-Host "Building frontend application..." -ForegroundColor Yellow
Set-Location ..
npm install
npm run build
# Deploy to Static Web Apps
Write-Host "Deploying to Azure Static Web Apps..." -ForegroundColor Yellow
az staticwebapp deploy `
--name $outputs.staticWebAppName.value `
--resource-group $ResourceGroupName `
--source dist/
Write-Host "🎉 Deployment completed successfully!" -ForegroundColor Green
Write-Host "🌐 Frontend URL: https://$($outputs.staticWebAppName.value).azurestaticapps.net" -ForegroundColor Cyan
Write-Host "⚡ Functions URL: https://$($outputs.functionAppName.value).azurewebsites.net" -ForegroundColor Cyan

53
deploy-simple.ps1 Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env pwsh
# Simple Azure deployment script
Write-Host "🚀 Deploying Miracles in Motion to Azure..." -ForegroundColor Green
# Deploy infrastructure
Write-Host "Deploying infrastructure..." -ForegroundColor Yellow
$deployment = az deployment group create `
--resource-group rg-miraclesinmotion-prod `
--template-file infrastructure/main.bicep `
--parameters @infrastructure/main.parameters.json `
--name "infra-deploy-$(Get-Date -Format 'yyyyMMdd-HHmmss')" `
--output json | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ Infrastructure deployment failed!"
exit 1
}
Write-Host "✅ Infrastructure deployed successfully!" -ForegroundColor Green
# Get deployment outputs
$functionAppName = $deployment.properties.outputs.functionAppName.value
$staticWebAppName = $deployment.properties.outputs.staticWebAppName.value
Write-Host "Function App: $functionAppName" -ForegroundColor Cyan
Write-Host "Static Web App: $staticWebAppName" -ForegroundColor Cyan
# Install Azure Functions Core Tools if needed
Write-Host "Checking Azure Functions Core Tools..." -ForegroundColor Yellow
try {
func --version
} catch {
Write-Host "Installing Azure Functions Core Tools..." -ForegroundColor Yellow
npm install -g azure-functions-core-tools@4 --unsafe-perm true
}
# Deploy Functions
Write-Host "Deploying Azure Functions..." -ForegroundColor Yellow
Set-Location api
func azure functionapp publish $functionAppName --typescript
Set-Location ..
# Deploy Static Web App
Write-Host "Deploying Static Web App..." -ForegroundColor Yellow
az staticwebapp deploy `
--name $staticWebAppName `
--resource-group rg-miraclesinmotion-prod `
--source dist/
Write-Host "🎉 Deployment completed successfully!" -ForegroundColor Green
Write-Host "🌐 Frontend URL: https://$staticWebAppName.azurestaticapps.net" -ForegroundColor Cyan
Write-Host "⚡ Functions URL: https://$functionAppName.azurewebsites.net" -ForegroundColor Cyan

323
infrastructure/main.bicep Normal file
View File

@@ -0,0 +1,323 @@
@description('Environment (dev, staging, prod)')
param environment string = 'prod'
@description('Azure region for resources')
param location string = resourceGroup().location
@description('Stripe public key for payments')
@secure()
param stripePublicKey string
// Variables
var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 6)
// Cosmos DB Account
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = {
name: 'mim-${environment}-${uniqueSuffix}-cosmos'
location: location
kind: 'GlobalDocumentDB'
properties: {
databaseAccountOfferType: 'Standard'
consistencyPolicy: {
defaultConsistencyLevel: 'Session'
}
locations: [
{
locationName: location
failoverPriority: 0
isZoneRedundant: false
}
]
capabilities: [
{
name: 'EnableServerless'
}
]
backupPolicy: {
type: 'Periodic'
periodicModeProperties: {
backupIntervalInMinutes: 240
backupRetentionIntervalInHours: 720
backupStorageRedundancy: 'Local'
}
}
}
}
// Cosmos DB Database
resource cosmosDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-04-15' = {
parent: cosmosAccount
name: 'MiraclesInMotion'
properties: {
resource: {
id: 'MiraclesInMotion'
}
}
}
// Cosmos DB Containers
resource donationsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = {
parent: cosmosDatabase
name: 'donations'
properties: {
resource: {
id: 'donations'
partitionKey: {
paths: ['/id']
kind: 'Hash'
}
indexingPolicy: {
indexingMode: 'consistent'
automatic: true
includedPaths: [
{
path: '/*'
}
]
}
}
}
}
resource volunteersContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = {
parent: cosmosDatabase
name: 'volunteers'
properties: {
resource: {
id: 'volunteers'
partitionKey: {
paths: ['/id']
kind: 'Hash'
}
}
}
}
resource programsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = {
parent: cosmosDatabase
name: 'programs'
properties: {
resource: {
id: 'programs'
partitionKey: {
paths: ['/id']
kind: 'Hash'
}
}
}
}
// Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: 'mim${environment}${uniqueSuffix}kv'
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: tenant().tenantId
accessPolicies: []
enabledForDeployment: false
enabledForDiskEncryption: false
enabledForTemplateDeployment: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
enableRbacAuthorization: true
}
}
// Application Insights
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: 'mim-${environment}-${uniqueSuffix}-insights'
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalyticsWorkspace.id
}
}
// Log Analytics Workspace
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: 'mim-${environment}-${uniqueSuffix}-logs'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
// Storage Account for Functions
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'mim${environment}${uniqueSuffix}st'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: 'mim-${environment}-${uniqueSuffix}-plan'
location: location
sku: {
name: 'Y1'
tier: 'Dynamic'
}
properties: {
reserved: false
}
}
// Function App
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
name: 'mim-${environment}-${uniqueSuffix}-func'
location: location
kind: 'functionapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
appSettings: [
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
}
{
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
}
{
name: 'WEBSITE_CONTENTSHARE'
value: toLower('mim-${environment}-func')
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'node'
}
{
name: 'WEBSITE_NODE_DEFAULT_VERSION'
value: '~18'
}
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
value: appInsights.properties.InstrumentationKey
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
{
name: 'COSMOS_CONNECTION_STRING'
value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString
}
{
name: 'COSMOS_DATABASE_NAME'
value: 'MiraclesInMotion'
}
{
name: 'KEY_VAULT_URL'
value: keyVault.properties.vaultUri
}
{
name: 'STRIPE_PUBLIC_KEY'
value: stripePublicKey
}
]
}
httpsOnly: true
}
}
// SignalR Service
resource signalR 'Microsoft.SignalRService/signalR@2023-02-01' = {
name: 'mim-${environment}-${uniqueSuffix}-signalr'
location: location
sku: {
name: 'Free_F1'
capacity: 1
}
kind: 'SignalR'
properties: {
features: [
{
flag: 'ServiceMode'
value: 'Serverless'
}
]
cors: {
allowedOrigins: ['*']
}
}
}
// Static Web App
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
name: 'mim-${environment}-${uniqueSuffix}-web'
location: 'Central US'
sku: {
name: 'Free'
}
properties: {
buildProperties: {
outputLocation: 'dist'
apiLocation: ''
appLocation: '/'
}
stagingEnvironmentPolicy: 'Enabled'
}
}
// Key Vault Secrets
resource cosmosConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
parent: keyVault
name: 'cosmos-connection-string'
properties: {
value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString
}
}
resource signalRConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
parent: keyVault
name: 'signalr-connection-string'
properties: {
value: signalR.listKeys().primaryConnectionString
}
}
// RBAC Assignments for Function App
resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, functionApp.id, 'Key Vault Secrets User')
scope: keyVault
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
principalId: functionApp.identity.principalId
principalType: 'ServicePrincipal'
}
}
// Outputs
output resourceGroupName string = resourceGroup().name
output cosmosAccountName string = cosmosAccount.name
output functionAppName string = functionApp.name
output staticWebAppName string = staticWebApp.name
output keyVaultName string = keyVault.name
output appInsightsName string = appInsights.name
output signalRName string = signalR.name
output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
output staticWebAppUrl string = 'https://${staticWebApp.properties.defaultHostname}'

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appName": {
"value": "miraclesinmotion"
},
"environment": {
"value": "prod"
},
"location": {
"value": "eastus2"
},
"stripePublicKey": {
"value": "pk_live_placeholder"
}
}
}

33
staticwebapp.config.json Normal file
View File

@@ -0,0 +1,33 @@
{
"routes": [
{
"route": "/api/*",
"allowedRoles": ["anonymous"]
},
{
"route": "/admin/*",
"allowedRoles": ["admin"]
},
{
"route": "/*",
"serve": "/index.html",
"statusCode": 200
}
],
"responseOverrides": {
"401": {
"redirect": "/login",
"statusCode": 302
}
},
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src 'self' https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https:; media-src 'self' https:; object-src 'none'; base-uri 'self'; form-action 'self' https:; frame-ancestors 'none'"
},
"mimeTypes": {
".json": "application/json",
".js": "text/javascript",
".css": "text/css"
}
}