@description('Environment (dev, staging, prod)') param environment string = 'prod' @description('Azure region for resources') param location string = resourceGroup().location @description('Stripe public key for payments') @secure() param stripePublicKey string @description('Azure AD Client ID for authentication') param azureClientId string = '' @description('Azure AD Tenant ID') param azureTenantId string = subscription().tenantId @description('Azure AD Client Secret (optional, for server-side flows)') @secure() param azureClientSecret string = '' @description('Custom domain name for the application') param customDomainName string = '' @description('Enable custom domain configuration') param enableCustomDomain bool = false @description('Static Web App SKU') @allowed(['Standard']) param staticWebAppSku string = 'Standard' @description('Function App SKU (Y1 for Consumption, EP1/EP2/EP3 for Premium)') @allowed(['Y1', 'EP1', 'EP2', 'EP3']) param functionAppSku string = 'Y1' // Variables var uniqueSuffix = substring(uniqueString(resourceGroup().id), 0, 6) var resourcePrefix = 'mim-${environment}-${uniqueSuffix}' // Log Analytics Workspace (needed first for Application Insights) resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: '${resourcePrefix}-logs' location: location properties: { sku: { name: 'PerGB2018' } retentionInDays: 30 features: { searchVersion: 1 legacy: 0 enableLogAccessUsingOnlyResourcePermissions: true } } } // Application Insights resource appInsights 'Microsoft.Insights/components@2020-02-02' = { name: '${resourcePrefix}-appinsights' location: location kind: 'web' properties: { Application_Type: 'web' Flow_Type: 'Redfield' Request_Source: 'IbizaAIExtension' RetentionInDays: 90 WorkspaceResourceId: logAnalyticsWorkspace.id IngestionMode: 'LogAnalytics' publicNetworkAccessForIngestion: 'Enabled' publicNetworkAccessForQuery: 'Enabled' } } // Key Vault resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { name: '${resourcePrefix}-kv' location: location properties: { sku: { family: 'A' name: 'standard' } tenantId: subscription().tenantId enableRbacAuthorization: true enableSoftDelete: true softDeleteRetentionInDays: 90 enablePurgeProtection: true networkAcls: { defaultAction: 'Allow' bypass: 'AzureServices' } } } // Cosmos DB Account - Production Ready resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = { name: '${resourcePrefix}-cosmos' location: location kind: 'GlobalDocumentDB' properties: { databaseAccountOfferType: 'Standard' consistencyPolicy: { defaultConsistencyLevel: 'Session' } locations: [ { locationName: location failoverPriority: 0 isZoneRedundant: true } ] enableAutomaticFailover: true enableMultipleWriteLocations: false backupPolicy: { type: 'Periodic' periodicModeProperties: { backupIntervalInMinutes: 240 backupRetentionIntervalInHours: 720 backupStorageRedundancy: 'Geo' } } networkAclBypass: 'AzureServices' publicNetworkAccess: 'Enabled' } } // Cosmos DB Database resource cosmosDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-05-15' = { parent: cosmosAccount name: 'MiraclesInMotion' properties: { resource: { id: 'MiraclesInMotion' } } } // Cosmos DB Containers resource donationsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = { parent: cosmosDatabase name: 'donations' properties: { resource: { id: 'donations' partitionKey: { paths: ['/id'] kind: 'Hash' } indexingPolicy: { indexingMode: 'consistent' automatic: true includedPaths: [ { path: '/*' } ] } } } } resource volunteersContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = { parent: cosmosDatabase name: 'volunteers' properties: { resource: { id: 'volunteers' partitionKey: { paths: ['/id'] kind: 'Hash' } } } } resource programsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = { parent: cosmosDatabase name: 'programs' properties: { resource: { id: 'programs' partitionKey: { paths: ['/id'] kind: 'Hash' } } } } resource studentsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-05-15' = { parent: cosmosDatabase name: 'students' properties: { resource: { id: 'students' partitionKey: { paths: ['/schoolId'] kind: 'Hash' } } } } // Function App Service Plan - Consumption Plan (Y1) for Production // Note: Changed from Premium to Consumption to avoid quota issues // Premium can be enabled later by requesting quota increase resource functionAppServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { name: '${resourcePrefix}-func-plan' location: location sku: { name: functionAppSku tier: functionAppSku == 'Y1' ? 'Dynamic' : 'ElasticPremium' size: functionAppSku != 'Y1' ? functionAppSku : null capacity: functionAppSku != 'Y1' ? 1 : null } kind: 'functionapp' properties: { reserved: functionAppSku != 'Y1' maximumElasticWorkerCount: functionAppSku != 'Y1' ? 20 : null } } // Storage Account for Function App resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: replace('${resourcePrefix}stor', '-', '') location: location sku: { name: 'Standard_LRS' } kind: 'StorageV2' properties: { supportsHttpsTrafficOnly: true encryption: { services: { file: { keyType: 'Account' enabled: true } blob: { keyType: 'Account' enabled: true } } keySource: 'Microsoft.Storage' } accessTier: 'Hot' } } // Function App with Enhanced Configuration resource functionApp 'Microsoft.Web/sites@2023-12-01' = { name: '${resourcePrefix}-func' location: location kind: 'functionapp,linux' identity: { type: 'SystemAssigned' } properties: { serverFarmId: functionAppServicePlan.id siteConfig: { linuxFxVersion: 'NODE|22' appSettings: [ { name: 'AzureWebJobsStorage' value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${az.environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' } { name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${az.environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' } { name: 'WEBSITE_CONTENTSHARE' value: toLower('${resourcePrefix}-func') } { name: 'FUNCTIONS_EXTENSION_VERSION' value: '~4' } { name: 'FUNCTIONS_WORKER_RUNTIME' value: 'node' } { name: 'WEBSITE_NODE_DEFAULT_VERSION' value: '~22' } { name: 'APPINSIGHTS_INSTRUMENTATIONKEY' value: appInsights.properties.InstrumentationKey } { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' value: appInsights.properties.ConnectionString } { name: 'COSMOS_CONNECTION_STRING' value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString } { name: 'COSMOS_DATABASE_NAME' value: 'MiraclesInMotion' } { name: 'KEY_VAULT_URL' value: keyVault.properties.vaultUri } { name: 'STRIPE_PUBLIC_KEY' value: stripePublicKey } ] cors: { allowedOrigins: ['*'] supportCredentials: false } use32BitWorkerProcess: false ftpsState: 'FtpsOnly' minTlsVersion: '1.2' } httpsOnly: true clientAffinityEnabled: false } } // SignalR Service - Standard for Production resource signalR 'Microsoft.SignalRService/signalR@2023-02-01' = { name: '${resourcePrefix}-signalr' location: location sku: { name: 'Standard_S1' capacity: 1 } kind: 'SignalR' properties: { features: [ { flag: 'ServiceMode' value: 'Serverless' } ] cors: { allowedOrigins: ['*'] } networkACLs: { defaultAction: 'Allow' } } } // Static Web App - Production Ready with Custom Domain Support resource staticWebApp 'Microsoft.Web/staticSites@2023-12-01' = { name: '${resourcePrefix}-web' location: 'Central US' sku: { name: staticWebAppSku tier: staticWebAppSku } properties: { buildProperties: { appLocation: '/' apiLocation: 'api' outputLocation: 'dist' } stagingEnvironmentPolicy: 'Enabled' allowConfigFileUpdates: true enterpriseGradeCdnStatus: 'Enabled' } } // Note: Static Web App authentication is configured via staticwebapp.config.json // and Azure Portal. App settings are configured separately through Azure Portal // or during deployment. The azureClientId and azureTenantId parameters are // stored in Key Vault for reference and can be used to configure authentication // in the Azure Portal after deployment. // Custom Domain Configuration (if enabled) // Note: Using TXT validation for Enterprise Grade Edge compatibility resource customDomain 'Microsoft.Web/staticSites/customDomains@2023-12-01' = if (enableCustomDomain && !empty(customDomainName)) { parent: staticWebApp name: customDomainName properties: { validationMethod: 'txt-token' } } // Key Vault Secrets resource cosmosConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { parent: keyVault name: 'cosmos-connection-string' properties: { value: cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString } } resource signalRConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { parent: keyVault name: 'signalr-connection-string' properties: { value: signalR.listKeys().primaryConnectionString } } resource stripeSecretKeySecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { parent: keyVault name: 'stripe-secret-key' properties: { value: 'sk_live_placeholder' // Replace with actual secret key } } // Azure AD Configuration Secrets resource azureClientIdSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = if (!empty(azureClientId)) { parent: keyVault name: 'azure-client-id' properties: { value: azureClientId } } resource azureTenantIdSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { parent: keyVault name: 'azure-tenant-id' properties: { value: azureTenantId } } resource azureClientSecretSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = if (!empty(azureClientSecret)) { parent: keyVault name: 'azure-client-secret' properties: { value: azureClientSecret } } // RBAC Assignments for Function App resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(keyVault.id, functionApp.id, 'Key Vault Secrets User') scope: keyVault properties: { roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User principalId: functionApp.identity.principalId principalType: 'ServicePrincipal' } } resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(cosmosAccount.id, functionApp.id, 'Cosmos DB Built-in Data Contributor') scope: cosmosAccount properties: { roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00000000-0000-0000-0000-000000000002') // Cosmos DB Built-in Data Contributor principalId: functionApp.identity.principalId principalType: 'ServicePrincipal' } } // Outputs output resourceGroupName string = resourceGroup().name output cosmosAccountName string = cosmosAccount.name output functionAppName string = functionApp.name output staticWebAppName string = staticWebApp.name output keyVaultName string = keyVault.name output appInsightsName string = appInsights.name output signalRName string = signalR.name output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.name output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}' output staticWebAppUrl string = 'https://${staticWebApp.properties.defaultHostname}' output customDomainName string = enableCustomDomain ? customDomainName : '' output applicationInsightsInstrumentationKey string = appInsights.properties.InstrumentationKey output applicationInsightsConnectionString string = appInsights.properties.ConnectionString output azureClientId string = azureClientId output azureTenantId string = azureTenantId output keyVaultUri string = keyVault.properties.vaultUri