diff --git a/AzureSaaSDevKit.sln b/AzureSaaSDevKit.sln
index e873ca2b..5a11969f 100644
--- a/AzureSaaSDevKit.sln
+++ b/AzureSaaSDevKit.sln
@@ -25,7 +25,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Admin.Web.Tests", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Catalog.Api.Tests", "src\Saas.Catalog\Saas.Catalog.Api.Tests\Saas.Catalog.Api.Tests.csproj", "{F76216F1-7410-4F10-A086-E3F676B962C3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtilities", "TestUtilities\TestUtilities.csproj", "{62537DAE-9BCD-4062-AD16-E71A6E731BAB}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "TestUtilities\TestUtilities.csproj", "{62537DAE-9BCD-4062-AD16-E71A6E731BAB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.LandingSignup.Web", "src\Saas.LandingSignup\Saas.LandingSignup.Web\Saas.LandingSignup.Web.csproj", "{71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}"
+EndProject
+Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "Saas.LandingSignup.Web.Deployment", "src\Saas.LandingSignup\Saas.LandingSignup.Web.Deployment\Saas.LandingSignup.Web.Deployment.deployproj", "{6A0836ED-483E-4F7E-8248-AC00FB52E778}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -81,6 +85,14 @@ Global
{62537DAE-9BCD-4062-AD16-E71A6E731BAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62537DAE-9BCD-4062-AD16-E71A6E731BAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62537DAE-9BCD-4062-AD16-E71A6E731BAB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {71694EE0-1D10-4DC3-B4EA-30C45A9FF31F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6A0836ED-483E-4F7E-8248-AC00FB52E778}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Saas.Deployment/Saas.Deployment.Root/azuredeploy.json b/src/Saas.Deployment/Saas.Deployment.Root/azuredeploy.json
index fa5d0658..0d05adf5 100644
--- a/src/Saas.Deployment/Saas.Deployment.Root/azuredeploy.json
+++ b/src/Saas.Deployment/Saas.Deployment.Root/azuredeploy.json
@@ -151,7 +151,7 @@
"apiVersion": "2021-04-01",
"properties": {
"templateLink": {
- "uri": "https://raw.githubusercontent.com/Azure/azure-saas/main/src/Saas.Provider/Saas.Provider.Web.Deployment/azuredeploy.json"
+ "uri": "https://raw.githubusercontent.com/Azure/azure-saas/main/src/Saas.Provider/Saas.LandingSignup.Web.Deployment/azuredeploy.json"
},
"parameters": {
"SaasProviderName": { "value": "[parameters('SaasProviderName')]" },
diff --git a/src/Saas.LandingSignup/README.md b/src/Saas.LandingSignup/README.md
new file mode 100644
index 00000000..2691eca5
--- /dev/null
+++ b/src/Saas.LandingSignup/README.md
@@ -0,0 +1,36 @@
+# SaaS Provider Web App
+
+The Saas Provider Web App is the marketing, onboarding and productivity component of Azure Saas for customers of your service. This should be thought of as the equivalent to https://www.office365.com where anonymous users are able to navigate to, explore the various service offerings / pricing tiers and onboard.
+
+[![Deploy to Azure](https://www.azuresaas.net/assets/images/deploy-to-azure.svg)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fmain%2Fsrc%2FSaas.Provider%2FSaas.Provider.Web.Deployment%2Fazuredeploy.json)
+
+Demo SaaS Service: https://www.azuresaas.net
+
+## Onboarding Flow
+The onboarding flow of the SaaS Provider Web App is a series of steps which persists the data of each step to Azure Cosmos DB upon submit of the given step.
+
+
+### Steps
+1. Enter Email Address
+ - Creates initial Identity on ASP.NET user account
+2. Enter Organization Name
+3. Enter Organization Category
+4. Select your Plan
+ - If 'Free' plan is selected, next step (billing) is skipped
+5. Enter billing information using Stripe payments
+ - Digital payments with Apple Pay, Google Pay, credit cards, etc.
+6. Creating your Subscriber
+ - Posts to Onboarding API to create new tenant
+7. Confirmation
+ - User is prompted to enter Password to complete Identity on ASP.NET user account
+ - User is sent email to validate email address
+
+
+
+## Azure Cosmos DB
+ The onboarding flow writes directly to Azure Cosmos DB for single-digit millisecond response times. The process generates a unique GUID unique identifier of the given user's flow and updates the JSON document with each property as the user proceeds through the six step process.
+
+## Identity on ASP.NET Core
+Upon entry of the first step of the onboarding flow (enter email address), the CreateController logic checks if an account exists with the given user name (email address). If an account does not exist with the email address, an Identity account is created with just a user name. Storing the email address allows for subsequent communication with the user in the case that they do not complete the onboarding flow.
+
+If the user does complete the onboarding flow, the final confirmation step will include a field to enter a password to complete their account creation.
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Deploy-AzureResourceGroup.ps1 b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Deploy-AzureResourceGroup.ps1
new file mode 100644
index 00000000..8926a049
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Deploy-AzureResourceGroup.ps1
@@ -0,0 +1,120 @@
+#Requires -Version 3.0
+
+Param(
+ [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation,
+ [string] $ResourceGroupName = 'Saas.Provider',
+ [switch] $UploadArtifacts,
+ [string] $StorageAccountName,
+ [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts',
+ [string] $TemplateFile = 'azuredeploy.json',
+ [string] $TemplateParametersFile = 'azuredeploy.parameters.json',
+ [string] $ArtifactStagingDirectory = '.',
+ [string] $DSCSourceFolder = 'DSC',
+ [switch] $ValidateOnly
+)
+
+try {
+ [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ','_'), '3.0.0')
+} catch { }
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version 3
+
+function Format-ValidationOutput {
+ param ($ValidationOutput, [int] $Depth = 0)
+ Set-StrictMode -Off
+ return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) })
+}
+
+$OptionalParameters = New-Object -TypeName Hashtable
+$TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile))
+$TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile))
+
+if ($UploadArtifacts) {
+ # Convert relative paths to absolute paths if needed
+ $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory))
+ $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder))
+
+ # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present
+ $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json
+ if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) {
+ $JsonParameters = $JsonParameters.parameters
+ }
+ $ArtifactsLocationName = '_artifactsLocation'
+ $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken'
+ $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select -Expand $ArtifactsLocationName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore
+ $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore
+
+ # Create DSC configuration archive
+ if (Test-Path $DSCSourceFolder) {
+ $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName})
+ foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
+ $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip'
+ Publish-AzVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose
+ }
+ }
+
+ # Create a storage account name if none was provided
+ if ($StorageAccountName -eq '') {
+ $StorageAccountName = 'stage' + ((Get-AzContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19)
+ }
+
+ $StorageAccount = (Get-AzStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName})
+
+ # Create the storage account if it doesn't already exist
+ if ($StorageAccount -eq $null) {
+ $StorageResourceGroupName = 'ARM_Deploy_Staging'
+ New-AzResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force
+ $StorageAccount = New-AzStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation"
+ }
+
+ # Generate the value for artifacts location if it is not provided in the parameter file
+ if ($OptionalParameters[$ArtifactsLocationName] -eq $null) {
+ $OptionalParameters[$ArtifactsLocationName] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName + '/'
+ }
+
+ # Copy files from the local storage staging location to the storage account container
+ New-AzStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1
+
+ $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName}
+ foreach ($SourcePath in $ArtifactFilePaths) {
+ Set-AzStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) `
+ -Container $StorageContainerName -Context $StorageAccount.Context -Force
+ }
+
+ # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file
+ if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) {
+ $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString -AsPlainText -Force `
+ (New-AzStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4))
+ }
+}
+
+# Create the resource group only when it doesn't already exist
+if ((Get-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue) -eq $null) {
+ New-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop
+}
+
+if ($ValidateOnly) {
+ $ErrorMessages = Format-ValidationOutput (Test-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName `
+ -TemplateFile $TemplateFile `
+ -TemplateParameterFile $TemplateParametersFile `
+ @OptionalParameters)
+ if ($ErrorMessages) {
+ Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.'
+ }
+ else {
+ Write-Output '', 'Template is valid.'
+ }
+}
+else {
+ New-AzResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) `
+ -ResourceGroupName $ResourceGroupName `
+ -TemplateFile $TemplateFile `
+ -TemplateParameterFile $TemplateParametersFile `
+ @OptionalParameters `
+ -Force -Verbose `
+ -ErrorVariable ErrorMessages
+ if ($ErrorMessages) {
+ Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") })
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Deployment.targets b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Deployment.targets
new file mode 100644
index 00000000..0d792ec6
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Deployment.targets
@@ -0,0 +1,123 @@
+
+
+
+ Debug
+ AnyCPU
+ bin\$(Configuration)\
+ false
+ true
+ false
+ None
+ obj\
+ $(BaseIntermediateOutputPath)\
+ $(BaseIntermediateOutputPath)$(Configuration)\
+ $(IntermediateOutputPath)ProjectReferences
+ $(ProjectReferencesOutputPath)\
+ true
+
+
+
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Never
+
+
+ false
+ Build
+
+
+
+
+
+
+
+ _GetDeploymentProjectContent;
+ _CalculateContentOutputRelativePaths;
+ _GetReferencedProjectsOutput;
+ _CalculateArtifactStagingDirectory;
+ _CopyOutputToArtifactStagingDirectory;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configuration=$(Configuration);Platform=$(Platform)
+
+
+
+
+
+
+ $([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)'))
+
+
+
+
+
+
+ $(OutDir)
+ $(OutputPath)
+ $(ArtifactStagingDirectory)\
+ $(ArtifactStagingDirectory)staging\
+ $(Build_StagingDirectory)
+
+
+
+
+
+
+ <_OriginalIdentity>%(DeploymentProjectContentOutput.Identity)
+ <_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', ''))
+
+
+
+
+ $(_RelativePath)
+
+
+
+
+
+
+
+
+ PrepareForRun
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Saas.LandingSignup.Web.Deployment.deployproj b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Saas.LandingSignup.Web.Deployment.deployproj
new file mode 100644
index 00000000..7bb4b85a
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/Saas.LandingSignup.Web.Deployment.deployproj
@@ -0,0 +1,34 @@
+
+
+
+
+ Debug
+ AnyCPU
+
+
+ Release
+ AnyCPU
+
+
+
+ 6a0836ed-483e-4f7e-8248-ac00fb52e778
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/azuredeploy.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/azuredeploy.json
new file mode 100644
index 00000000..2a251eb4
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/azuredeploy.json
@@ -0,0 +1,160 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "SaasProviderName": {
+ "type": "string",
+ "defaultValue": "contoso"
+ },
+ "SaasEnvironment": {
+ "type": "string",
+ "defaultValue": "dev",
+ "allowedValues": [
+ "dev",
+ "staging",
+ "test",
+ "prod"
+ ]
+ },
+ "SaasLocation": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "Location for the Cosmos DB account."
+ }
+ },
+ "SaasInstanceNumber": {
+ "type": "string",
+ "defaultValue": "001"
+ },
+ "CosmosDbEndpoint": {
+ "type": "string"
+ },
+ "CosmosDbAccountKey": {
+ "type": "string",
+ "metadata": {
+ "description": "The account key output of CosmosDB"
+ }
+ },
+ "CosmosDbAccountName": {
+ "type": "string",
+ "defaultValue": "[concat('cosmos-', parameters('SaaSProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]",
+ "metadata": {
+ "description": "Cosmos DB account name"
+ }
+ },
+ "CosmosDbDatabaseName": {
+ "type": "string",
+ "defaultValue": "azuresaas",
+ "metadata": {
+ "description": "The name for the Core (SQL) database"
+ }
+ },
+ "CosmosDbConnectionString": {
+ "type": "string"
+ },
+ "IdentityDbConnectionString": {
+ "type": "string"
+ },
+ "CatalogDbConnectionString": {
+ "type": "string"
+ }
+ },
+ "variables": {
+ "appServicePlanName": "[concat('app-', parameters('SaasProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]",
+ "providerWebAppName": "[concat('app-', parameters('SaasProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'))]"
+ },
+ "resources": [
+ {
+ "name": "[variables('appServicePlanName')]",
+ "type": "Microsoft.Web/serverfarms",
+ "location": "[resourceGroup().location]",
+ "apiVersion": "2015-08-01",
+ "sku": {
+ "name": "F1"
+ },
+ "dependsOn": [],
+ "tags": {
+ "displayName": "SaaS Provider App Service Plan"
+ },
+ "properties": {
+ "name": "[variables('appServicePlanName')]",
+ "numberOfWorkers": 1
+ },
+ "resources": [
+ {
+ "name": "[variables('providerWebAppName')]",
+ "type": "Microsoft.Web/sites",
+ "location": "[resourceGroup().location]",
+ "apiVersion": "2015-08-01",
+ "dependsOn": [
+ "[concat('Microsoft.Web/serverFarms/', variables('appServicePlanName'))]"
+ ],
+ "tags": {
+ "displayName": "SaaS Provider Web App"
+ },
+ "properties": {
+ "name": "[variables('providerWebAppName')]",
+ "serverFarmId": "[resourceId(resourceGroup().name, 'Microsoft.Web/serverFarms', variables('appServicePlanName'))]",
+ "siteConfig": {
+ "netFrameworkVersion": "v5.0",
+ "appSettings": [
+ {
+ "name": "ASPNETCORE_ENVIRONMENT",
+ "value": "Production"
+ },
+ {
+ "name": "AppSettings:CosmosDb:Account",
+ "value": "[parameters('CosmosDbEndpoint')]"
+ },
+ {
+ "name": "AppSettings:CosmosDb:Key",
+ "value": "[parameters('CosmosDbAccountKey')]"
+ },
+ {
+ "name": "AppSettings:CosmosDb:DatabaseName",
+ "value": "[parameters('CosmosDbDatabaseName')]"
+ },
+ {
+ "name": "AppSettings:CosmosDb:ContainerName",
+ "value": "OnboardingFlow"
+ },
+ {
+ "name": "AppSettings:OnboardingApiBaseUrl",
+ "value": "[concat('https://api-onboarding-', parameters('SaasProviderName'), '-', parameters('SaasEnvironment'), '-', parameters('SaasInstanceNumber'), '.azurewebsites.net/')]"
+ }
+ ],
+ "connectionStrings": [
+ {
+ "name": "CosmosDb",
+ "connectionString": "[parameters('CosmosDbConnectionString')]",
+ "type": "Custom"
+ },
+ {
+ "name": "IdentityDbConnection",
+ "connectionString": "[parameters('IdentityDbConnectionString')]",
+ "type": "SQLAzure"
+ }
+ ]
+ }
+ },
+ "resources": [
+ {
+ "name": "MSDeploy",
+ "type": "extensions",
+ "location": "[resourceGroup().location]",
+ "apiVersion": "2015-08-01",
+ "dependsOn": [ "[resourceId('Microsoft.Web/sites', variables('providerWebAppName'))]" ],
+ "tags": { "displayName": "Deploy" },
+ "properties": {
+ "packageUri": "https://stsaasdev001.blob.core.windows.net/artifacts/saas-provider/Saas.LandingSignup.Web.zip?sv=2020-04-08&st=2021-06-07T19%3A23%3A20Z&se=2022-06-08T19%3A23%3A00Z&sr=c&sp=rl&sig=kNf0qwTfaCJg02xYeUHlfmHOJvI1bGU1HftjUJ5hl5o%3D"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "outputs": {
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/azuredeploy.parameters.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/azuredeploy.parameters.json
new file mode 100644
index 00000000..34cbd1cb
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web.Deployment/azuredeploy.parameters.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "SaasProviderName": {
+ "value": "org1"
+ },
+ "SaasEnvironment": {
+ "value": "dev"
+ },
+ "SaasInstanceNumber": {
+ "value": "028"
+ },
+ "SaasLocation": {
+ "value": "eastus"
+ },
+ "CosmosDbConnectionString": {
+ "value": "AccountEndpoint=https://cosmos-org1-dev-028.documents.azure.com:443/;AccountKey=J92lmfHRM5pGvUJxEqhDqa5LuwGtcMntoV4j9x0sMin52sgMyQKflkFR9kMYtwOockyMVMcYbE4wK6eK1jbL7Q==;"
+ },
+ "CosmosDbAccountKey": {
+ "value": "J92lmfHRM5pGvUJxEqhDqa5LuwGtcMntoV4j9x0sMin52sgMyQKflkFR9kMYtwOockyMVMcYbE4wK6eK1jbL7Q=="
+ },
+ "CosmosDbEndpoint": {
+ "value": "https://cosmos-org1-dev-028.documents.azure.com:443/"
+ },
+ "CosmosDbDatabaseName": {
+ "value": "azuresaas028"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/.config/dotnet-tools.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web/.config/dotnet-tools.json
new file mode 100644
index 00000000..b0e38abd
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/.config/dotnet-tools.json
@@ -0,0 +1,5 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {}
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Areas/Identity/Pages/_ViewStart.cshtml b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Areas/Identity/Pages/_ViewStart.cshtml
new file mode 100644
index 00000000..c4284f6c
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Areas/Identity/Pages/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "/Views/Shared/_Layout.cshtml";
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/AdminController.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/AdminController.cs
new file mode 100644
index 00000000..7bbb12c8
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/AdminController.cs
@@ -0,0 +1,25 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Saas.LandingSignup.Web.Controllers
+{
+ public class AdminController : Controller
+ {
+ [Route("{tenant}/admin/dashboard")]
+ public IActionResult Dashboard()
+ {
+ return View();
+ }
+
+ [Route("{tenant}/admin/manage")]
+ public IActionResult Manage()
+ {
+ return View();
+ }
+
+ [Route("{tenant}/admin/users")]
+ public IActionResult Users()
+ {
+ return View();
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/CreateController.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/CreateController.cs
new file mode 100644
index 00000000..c9152a14
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/CreateController.cs
@@ -0,0 +1,349 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Saas.LandingSignup.Web.Models;
+using Saas.LandingSignup.Web.Models.CosmosDb;
+using Saas.LandingSignup.Web.Services;
+using System;
+using System.Collections.Generic;
+using System.Data.SqlClient;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Controllers
+{
+ public class CreateController : Controller
+ {
+ private const string AuthorityFormat = "https://login.microsoftonline.com/{0}/v2.0";
+
+ private readonly ILogger _logger;
+ private readonly AppSettings _appSettings;
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+ private readonly ICosmosDbService _cosmosDbService;
+
+ public CreateController(ILogger logger, IOptions appSettings, UserManager userManager, SignInManager signInManager, ICosmosDbService cosmosDbService)
+ {
+ _logger = logger;
+ _appSettings = appSettings.Value;
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _cosmosDbService = cosmosDbService;
+ }
+
+ [Route("/create")]
+ public IActionResult Index()
+ {
+ return View();
+ }
+
+ [Route("/create/name")]
+ public IActionResult Name(string id, string userId, string isExistingUser, string userNameExists)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ string processId = Guid.NewGuid().ToString();
+ ViewBag.Id = processId;
+ }
+ else
+ {
+ ViewBag.Id = id;
+ }
+
+ // Populate hidden input fields
+ ViewBag.UserId = userId;
+ ViewBag.IsExistingUser = isExistingUser;
+ ViewBag.userNameExists = userNameExists;
+
+ return View();
+ }
+
+ [Route("/create/name")]
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task NameAsync(string id, string userId, string isExistingUser, string name)
+ {
+ // Create order process id and object
+ Item item = new Item()
+ {
+ Id = id,
+ Name = "Onboarding Flow",
+ TenantName = name,
+ UserId = userId,
+ IsExistingUser = isExistingUser,
+ IpAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ Created = DateTime.Now
+ };
+
+ // Commit to CosmosDB
+ try
+ {
+ await _cosmosDbService.UpdateItemAsync(id, item);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogInformation(ex.ToString());
+ }
+
+ return RedirectToAction("category", "create", new { item.Id, userId, isExistingUser, name });
+ }
+
+ [Route("/create/category")]
+ public IActionResult Category(string id, string userId, string isExistingUser, string name)
+ {
+ // Populate hidden input fields
+ ViewBag.Id = id;
+ ViewBag.UserId = userId;
+ ViewBag.IsExistingUser = isExistingUser;
+ ViewBag.Name = name;
+
+ // Populate Categories dropdown list
+ List categories = new List();
+ categories.Add(new Category { Id = 1, Name = "Automotive, Mobility & Transportation" });
+ categories.Add(new Category { Id = 2, Name = "Energy & Sustainability" });
+ categories.Add(new Category { Id = 3, Name = "Financial Services" });
+ categories.Add(new Category { Id = 4, Name = "Healthcare & Life Sciences" });
+ categories.Add(new Category { Id = 5, Name = "Manufacturing & Supply Chain" });
+ categories.Add(new Category { Id = 6, Name = "Media & Communications" });
+ categories.Add(new Category { Id = 7, Name = "Public Sector" });
+ categories.Add(new Category { Id = 8, Name = "Retail & Consumer Goods" });
+ categories.Add(new Category { Id = 9, Name = "Software" });
+
+ return View(categories);
+ }
+
+ [Route("/create/category")]
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task CategoryAsync(string id, string userId, string isExistingUser, string name, int categoryId)
+ {
+ // Recreate order process id and object
+ Item item = new Item()
+ {
+ Id = id,
+ Name = "Onboarding Flow",
+ TenantName = name,
+ UserId = userId,
+ IsExistingUser = isExistingUser,
+ CategoryId = categoryId,
+ IpAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ Created = DateTime.Now
+ };
+
+ // Update order process in CosmosDB
+ try
+ {
+ await _cosmosDbService.UpdateItemAsync(id, item);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogInformation(ex.ToString());
+ }
+
+ return RedirectToAction("plans", "create", new { id, userId, isExistingUser, name, categoryId });
+ }
+
+ [Route("/create/plans")]
+ public IActionResult Plans(string id, string userId, string isExistingUser, string name, int categoryId)
+ {
+ // Populate hidden input fields
+ ViewBag.Id = id;
+ ViewBag.UserId = userId;
+ ViewBag.IsExistingUser = isExistingUser;
+ ViewBag.Name = name;
+ ViewBag.CategoryId = categoryId;
+
+ return View();
+ }
+
+ [Route("/create/plans")]
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task PlansAsync(string id, string userId, string isExistingUser, string name, int categoryId, int productId)
+ {
+ // Recreate order process id and object
+ Item item = new Item()
+ {
+ Id = id,
+ Name = "Onboarding Flow",
+ TenantName = name,
+ UserId = userId,
+ IsExistingUser = isExistingUser,
+ CategoryId = categoryId,
+ ProductId = productId,
+ IpAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ Created = DateTime.Now
+ };
+
+ // Update order process in CosmosDB
+ try
+ {
+ await _cosmosDbService.UpdateItemAsync(id, item);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogInformation(ex.ToString());
+ }
+
+ if (productId == 5)
+ {
+ return RedirectToAction("deploy", "create", new { id, userId, isExistingUser, name, categoryId, productId });
+ }
+ else if (productId == 6)
+ {
+ return RedirectToAction("merchant", "create", new { id, userId, isExistingUser, name, categoryId, productId });
+ }
+ else
+ {
+ return RedirectToAction("merchant", "create", new { id, userId, isExistingUser, name, categoryId, productId });
+ }
+ }
+
+ [Route("/create/merchant")]
+ public IActionResult Merchant(string id, string userId, string isExistingUser, string name, int categoryId, int productId)
+ {
+ // Populate hidden input fields
+ ViewBag.UserId = id;
+ ViewBag.UserId = userId;
+ ViewBag.IsExistingUser = isExistingUser;
+ ViewBag.Name = name;
+ ViewBag.CategoryId = categoryId;
+ ViewBag.ProductId = productId;
+ ViewBag.StripeProductPlanSubscriberBasic = _appSettings.StripeProductPlanSubscriberBasic;
+ ViewBag.StripeProductPlanSubscriberStandard = _appSettings.StripeProductPlanSubscriberStandard;
+ ViewBag.StripePublishableKey = _appSettings.StripePublishableKey;
+
+ return View();
+ }
+
+ [Route("/create/deploy")]
+ public async Task DeployAsync(string id, string userId, string isExistingUser, string name, int categoryId, int productId)
+ {
+ HttpClient httpClient = new HttpClient();
+ OnboardingClient onboardingClient = new OnboardingClient(_appSettings.OnboardingApiBaseUrl, httpClient);
+
+ Services.Tenant tenant = new Services.Tenant()
+ {
+ Id = Guid.NewGuid(),
+ Name = name,
+ IsActive = true,
+ IsCancelled = false,
+ IsProvisioned = true,
+ ApiKey = Guid.NewGuid(),
+ CategoryId = categoryId,
+ ProductId = productId,
+ UserId = userId,
+ };
+
+ await onboardingClient.TenantsPOSTAsync(tenant);
+
+ // Recreate order process id and object and set IsComplete = true
+ Item item = new Item()
+ {
+ Id = id,
+ Name = "Onboarding Flow",
+ TenantName = name,
+ UserId = userId,
+ IsExistingUser = isExistingUser,
+ CategoryId = categoryId,
+ ProductId = productId,
+ IsComplete = true,
+ IpAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ Created = DateTime.Now
+ };
+
+ // Update order process in CosmosDB
+ try
+ {
+ await _cosmosDbService.UpdateItemAsync(id, item);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogInformation(ex.ToString());
+ }
+
+ return RedirectToAction("confirmation", "create", new { isExistingUser });
+ }
+
+ [Route("/create/confirmation")]
+ public IActionResult Confirmation(string isExistingUser)
+ {
+ return View();
+ }
+
+ [Route("/create/confirmation")]
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public IActionResult Confirmation([Bind("Email,Password,TenantId,TenantUserName")] OnboardingFlow onboardingFlow)
+ {
+ return View();
+ }
+
+ protected int CreateCustomer(string email)
+ {
+ // Create customer
+ using (SqlConnection connection = new SqlConnection("Data Source=tcp:ma.database.windows.net;Initial Catalog=ma-provider-sql;User Id=modernappz;Password=N0sK3Tamanda;"))
+ {
+ connection.Open();
+
+ using (SqlCommand cmd = new SqlCommand("usp_CreateCustomer", connection))
+ {
+ cmd.CommandType = System.Data.CommandType.StoredProcedure;
+ cmd.Parameters.AddWithValue("@email", email);
+ //cmd.Parameters.AddWithValue("@userId", User.Identity.GetUserId());
+
+ SqlParameter returnValue = new SqlParameter();
+ returnValue.Direction = System.Data.ParameterDirection.ReturnValue;
+ cmd.Parameters.Add(returnValue);
+
+ cmd.ExecuteNonQuery();
+
+ int customerId = (int)returnValue.Value;
+
+ return customerId;
+ }
+ }
+ }
+
+ protected int CreateOrder(string userId, int productId)
+ {
+ // Create order
+ using (SqlConnection connection = new SqlConnection("Data Source=tcp:ma.database.windows.net;Initial Catalog=ma-provider-sql;User Id=modernappz;Password=N0sK3Tamanda;"))
+ {
+ connection.Open();
+
+ using (SqlCommand cmd = new SqlCommand("usp_CreateOrder", connection))
+ {
+ cmd.CommandType = System.Data.CommandType.StoredProcedure;
+ cmd.Parameters.AddWithValue("@productId", productId);
+ cmd.Parameters.AddWithValue("@userId", userId);
+
+ SqlParameter returnValue = new SqlParameter();
+ returnValue.Direction = System.Data.ParameterDirection.ReturnValue;
+ cmd.Parameters.Add(returnValue);
+
+ cmd.ExecuteNonQuery();
+
+ int orderId = (int)returnValue.Value;
+
+ return orderId;
+ }
+ }
+ }
+
+ private string Clean(string s)
+ {
+ return new StringBuilder(s)
+ .Replace("&", "and")
+ .Replace(",", "")
+ .Replace(" ", "")
+ .Replace(" ", "")
+ .Replace("'", "")
+ .Replace(".", "")
+ .ToString()
+ .ToLower();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/CustomerController.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/CustomerController.cs
new file mode 100644
index 00000000..cd60fe51
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/CustomerController.cs
@@ -0,0 +1,58 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Saas.LandingSignup.Web.Models;
+using Saas.LandingSignup.Web.Repositories;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Controllers
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class CustomersController : ControllerBase
+ {
+ private readonly IConfiguration _configuration;
+ private readonly CustomerRepository _customerRepository;
+ private readonly TenantRepository _tenantRepository;
+
+ public CustomersController(CustomerRepository customerRepository, TenantRepository tenantRepository, IConfiguration configuration)
+ {
+ _configuration = configuration;
+ _customerRepository = customerRepository;
+ _tenantRepository = tenantRepository;
+ }
+
+ [HttpGet]
+ public async Task> GetAll()
+ {
+ await AddTenantIdToSession();
+
+ return await _customerRepository.GetAllCustomers(HttpContext.Session.GetString("TenantId"));
+ }
+
+ public async Task AddTenantIdToSession()
+ {
+ string tenantIdentifier = HttpContext.Session.GetString("TenantId");
+ if (string.IsNullOrEmpty(tenantIdentifier))
+ {
+ var apiKey = HttpContext.Request.Headers["X-Api-Key"].FirstOrDefault();
+ if (string.IsNullOrEmpty(apiKey))
+ {
+ return;
+ }
+
+ Guid apiKeyGuid;
+ if (!Guid.TryParse(apiKey, out apiKeyGuid))
+ {
+ return;
+ }
+
+ string tenantId = await _tenantRepository.GetTenantId(apiKeyGuid);
+ HttpContext.Session.SetString("TenantId", tenantId);
+ }
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/HomeController.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/HomeController.cs
new file mode 100644
index 00000000..efc20754
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/HomeController.cs
@@ -0,0 +1,99 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Saas.LandingSignup.Web.Models;
+using Saas.LandingSignup.Web.Models.CosmosDb;
+using Saas.LandingSignup.Web.Services;
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Controllers
+{
+ public class HomeController : Controller
+ {
+ private readonly ILogger _logger;
+ private readonly UserManager _userManager;
+ private readonly ICosmosDbService _cosmosDbService;
+
+ public HomeController(ILogger logger, UserManager userManager, ICosmosDbService cosmosDbService)
+ {
+ _logger = logger;
+ _userManager = userManager;
+ _cosmosDbService = cosmosDbService;
+ }
+
+ public IActionResult Help()
+ {
+ return View();
+ }
+
+ public IActionResult Index()
+ {
+ return View();
+ }
+
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task IndexAsync(string emailAddress)
+ {
+ ViewBag.EmailAddress = emailAddress;
+
+ // Do a check to see if username already taken
+ var user = new ApplicationUser { UserName = emailAddress, Email = emailAddress };
+ var result = await _userManager.CreateAsync(user);
+
+ if (result.Succeeded)
+ {
+ // Create order process id and object
+ Item item = new Item()
+ {
+ Id = Guid.NewGuid().ToString(),
+ Name = "Onboarding Flow",
+ UserId = user.Id,
+ IsExistingUser = "false",
+ IpAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ Created = DateTime.Now
+ };
+
+ // Commit to CosmosDB
+ try
+ {
+ await _cosmosDbService.AddItemAsync(item);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogInformation(ex.ToString());
+ }
+
+ return RedirectToAction("name", "create", new { id = item.Id, userId = user.Id, isExistingUser = "false" });
+ }
+ else
+ {
+ foreach (var error in result.Errors)
+ {
+ ModelState.AddModelError(string.Empty, error.Description);
+ }
+ }
+
+ // If we got this far, something failed, redisplay view
+ return View();
+ }
+
+ public IActionResult Pricing()
+ {
+ return View();
+ }
+
+ public IActionResult Privacy()
+ {
+ return View();
+ }
+
+ [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+ public IActionResult Error()
+ {
+ return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/RequestHeaderController.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/RequestHeaderController.cs
new file mode 100644
index 00000000..e12485a6
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/RequestHeaderController.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Saas.LandingSignup.Web.Controllers
+{
+ public class RequestHeaderController : Controller
+ {
+ public IActionResult Index()
+ {
+ return View();
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/RequestPathController.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/RequestPathController.cs
new file mode 100644
index 00000000..7f37da40
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Controllers/RequestPathController.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Saas.LandingSignup.Web.Controllers
+{
+ [Route("/{tenant}")]
+ public class RequestPathController : Controller
+ {
+ [Route("/{tenant}")]
+ public IActionResult Index(string tenant)
+ {
+ ViewBag.Tenant = tenant;
+
+ return View();
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/ApplicationDbContext.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/ApplicationDbContext.cs
new file mode 100644
index 00000000..045f270a
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/ApplicationDbContext.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using Saas.LandingSignup.Web.Models;
+
+namespace Saas.LandingSignup.Web.Data
+{
+ public class ApplicationDbContext : IdentityDbContext
+ {
+ public ApplicationDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
new file mode 100644
index 00000000..e21230f7
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
@@ -0,0 +1,277 @@
+//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Saas.LandingSignup.Web.Data;
+using System;
+
+namespace Saas.LandingSignup.Web.Data.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("00000000000000_CreateIdentitySchema")]
+ partial class CreateIdentitySchema
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "3.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128)
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(256)")
+ .HasMaxLength(256);
+
+ b.Property("NormalizedName")
+ .HasColumnType("nvarchar(256)")
+ .HasMaxLength(256);
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasColumnType("nvarchar(256)")
+ .HasMaxLength(256);
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("nvarchar(256)")
+ .HasMaxLength(256);
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("nvarchar(256)")
+ .HasMaxLength(256);
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasColumnType("nvarchar(256)")
+ .HasMaxLength(256);
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(128)")
+ .HasMaxLength(128);
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(128)")
+ .HasMaxLength(128);
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(128)")
+ .HasMaxLength(128);
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(128)")
+ .HasMaxLength(128);
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs
new file mode 100644
index 00000000..d0c38cca
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs
@@ -0,0 +1,221 @@
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using System;
+
+namespace Saas.LandingSignup.Web.Data.Migrations
+{
+ public partial class CreateIdentitySchema : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AspNetRoles",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false),
+ Name = table.Column(maxLength: 256, nullable: true),
+ NormalizedName = table.Column(maxLength: 256, nullable: true),
+ ConcurrencyStamp = table.Column(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoles", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUsers",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false),
+ UserName = table.Column(maxLength: 256, nullable: true),
+ NormalizedUserName = table.Column(maxLength: 256, nullable: true),
+ Email = table.Column(maxLength: 256, nullable: true),
+ NormalizedEmail = table.Column(maxLength: 256, nullable: true),
+ EmailConfirmed = table.Column(nullable: false),
+ PasswordHash = table.Column(nullable: true),
+ SecurityStamp = table.Column(nullable: true),
+ ConcurrencyStamp = table.Column(nullable: true),
+ PhoneNumber = table.Column(nullable: true),
+ PhoneNumberConfirmed = table.Column(nullable: false),
+ TwoFactorEnabled = table.Column(nullable: false),
+ LockoutEnd = table.Column(nullable: true),
+ LockoutEnabled = table.Column(nullable: false),
+ AccessFailedCount = table.Column(nullable: false),
+ TenantId = table.Column(maxLength: 37, nullable: true),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUsers", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetRoleClaims",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false)
+ .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
+ RoleId = table.Column(nullable: false),
+ ClaimType = table.Column(nullable: true),
+ ClaimValue = table.Column(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserClaims",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false)
+ .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
+ UserId = table.Column(nullable: false),
+ ClaimType = table.Column(nullable: true),
+ ClaimValue = table.Column(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetUserClaims_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserLogins",
+ columns: table => new
+ {
+ LoginProvider = table.Column(maxLength: 128, nullable: false),
+ ProviderKey = table.Column(maxLength: 128, nullable: false),
+ ProviderDisplayName = table.Column(nullable: true),
+ UserId = table.Column(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
+ table.ForeignKey(
+ name: "FK_AspNetUserLogins_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserRoles",
+ columns: table => new
+ {
+ UserId = table.Column(nullable: false),
+ RoleId = table.Column(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserTokens",
+ columns: table => new
+ {
+ UserId = table.Column(nullable: false),
+ LoginProvider = table.Column(maxLength: 128, nullable: false),
+ Name = table.Column(maxLength: 128, nullable: false),
+ Value = table.Column(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
+ table.ForeignKey(
+ name: "FK_AspNetUserTokens_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetRoleClaims_RoleId",
+ table: "AspNetRoleClaims",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "RoleNameIndex",
+ table: "AspNetRoles",
+ column: "NormalizedName",
+ unique: true,
+ filter: "[NormalizedName] IS NOT NULL");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserClaims_UserId",
+ table: "AspNetUserClaims",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserLogins_UserId",
+ table: "AspNetUserLogins",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserRoles_RoleId",
+ table: "AspNetUserRoles",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "EmailIndex",
+ table: "AspNetUsers",
+ column: "NormalizedEmail");
+
+ migrationBuilder.CreateIndex(
+ name: "UserNameIndex",
+ table: "AspNetUsers",
+ column: "NormalizedUserName",
+ unique: true,
+ filter: "[NormalizedUserName] IS NOT NULL");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AspNetRoleClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserLogins");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserTokens");
+
+ migrationBuilder.DropTable(
+ name: "AspNetRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUsers");
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/20210411071634_AddApplicationUser.Designer.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/20210411071634_AddApplicationUser.Designer.cs
new file mode 100644
index 00000000..a45a19de
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/20210411071634_AddApplicationUser.Designer.cs
@@ -0,0 +1,280 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Saas.LandingSignup.Web.Data;
+
+namespace Saas.LandingSignup.Web.Data.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20210411071634_AddApplicationUser")]
+ partial class AddApplicationUser
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("Relational:MaxIdentifierLength", 128)
+ .HasAnnotation("ProductVersion", "5.0.5")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ProviderKey")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Name")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens");
+ });
+
+ modelBuilder.Entity("Saas.LandingSignup.Web.Data.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TenantId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/20210411071634_AddApplicationUser.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/20210411071634_AddApplicationUser.cs
new file mode 100644
index 00000000..53f573de
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/20210411071634_AddApplicationUser.cs
@@ -0,0 +1,23 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Saas.LandingSignup.Web.Data.Migrations
+{
+ public partial class AddApplicationUser : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "TenantId",
+ table: "AspNetUsers",
+ type: "nvarchar(37)",
+ nullable: false);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "TenantId",
+ table: "AspNetUsers");
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/ApplicationDbContextModelSnapshot.cs
new file mode 100644
index 00000000..4ea79839
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Data/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -0,0 +1,278 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Saas.LandingSignup.Web.Data;
+
+namespace Saas.LandingSignup.Web.Data.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ partial class ApplicationDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("Relational:MaxIdentifierLength", 128)
+ .HasAnnotation("ProductVersion", "5.0.5")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ProviderKey")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Name")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens");
+ });
+
+ modelBuilder.Entity("Saas.LandingSignup.Web.Data.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TenantId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/AppSettings.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/AppSettings.cs
new file mode 100644
index 00000000..4e5e689d
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/AppSettings.cs
@@ -0,0 +1,14 @@
+namespace Saas.LandingSignup.Web.Models
+{
+ public class AppSettings
+ {
+ public string RedirectUri { get; set; }
+ public CosmosDb.Instance CosmosDbInstance { get; set; }
+ public string SendGridAPIKey { get; set; }
+ public string StripePublishableKey { get; set; }
+ public string StripeSecretKey { get; set; }
+ public string StripeProductPlanSubscriberBasic { get; set; }
+ public string StripeProductPlanSubscriberStandard { get; set; }
+ public string OnboardingApiBaseUrl { get; set; }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/ApplicationUser.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/ApplicationUser.cs
new file mode 100644
index 00000000..c13bacc8
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/ApplicationUser.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace Saas.LandingSignup.Web.Models
+{
+ public class ApplicationUser : IdentityUser
+ {
+
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Category.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Category.cs
new file mode 100644
index 00000000..24f3ef67
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Category.cs
@@ -0,0 +1,10 @@
+#nullable disable
+
+namespace Saas.LandingSignup.Web.Models
+{
+ public partial class Category
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/CosmosDb/Instance.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/CosmosDb/Instance.cs
new file mode 100644
index 00000000..24932bf0
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/CosmosDb/Instance.cs
@@ -0,0 +1,10 @@
+namespace Saas.LandingSignup.Web.Models.CosmosDb
+{
+ public class Instance
+ {
+ public string Account { get; set; }
+ public string Key { get; set; }
+ public string DatabaseName { get; set; }
+ public string ContainerName { get; set; }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/CosmosDb/Item.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/CosmosDb/Item.cs
new file mode 100644
index 00000000..d63bc211
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/CosmosDb/Item.cs
@@ -0,0 +1,38 @@
+using Newtonsoft.Json;
+using System;
+
+namespace Saas.LandingSignup.Web.Models.CosmosDb
+{
+ public class Item
+ {
+ [JsonProperty(PropertyName = "id")]
+ public string Id { get; set; }
+
+ [JsonProperty(PropertyName = "name")]
+ public string Name { get; set; }
+
+ [JsonProperty(PropertyName = "tenantName")]
+ public string TenantName { get; set; }
+
+ [JsonProperty(PropertyName = "userId")]
+ public string UserId { get; set; }
+
+ [JsonProperty(PropertyName = "isExistingUser")]
+ public string IsExistingUser { get; set; }
+
+ [JsonProperty(PropertyName = "categoryId")]
+ public int CategoryId { get; set; }
+
+ [JsonProperty(PropertyName = "productId")]
+ public int ProductId { get; set; }
+
+ [JsonProperty(PropertyName = "isComplete")]
+ public bool IsComplete { get; set; }
+
+ [JsonProperty(PropertyName = "ipAddress")]
+ public string IpAddress { get; set; }
+
+ [JsonProperty(PropertyName = "created")]
+ public DateTime Created { get; set; }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Customer.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Customer.cs
new file mode 100644
index 00000000..4f0fc571
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Customer.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Saas.LandingSignup.Web.Models
+{
+ public class Customer
+ {
+ public Guid Id { get; set; }
+ public Guid TenantId { get; set; }
+ public string CustomerName { get; set; }
+ public bool IsActive { get; set; }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/ErrorViewModel.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/ErrorViewModel.cs
new file mode 100644
index 00000000..00ac9edc
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/ErrorViewModel.cs
@@ -0,0 +1,9 @@
+namespace Saas.LandingSignup.Web.Models
+{
+ public class ErrorViewModel
+ {
+ public string RequestId { get; set; }
+
+ public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/OnboardingFlow.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/OnboardingFlow.cs
new file mode 100644
index 00000000..c0fcbc5d
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/OnboardingFlow.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Saas.LandingSignup.Web.Models
+{
+ public class OnboardingFlow
+ {
+ [Required]
+ [EmailAddress]
+ [Display(Name = "Email")]
+ public string Email { get; set; }
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ [Display(Name = "Password")]
+ public string Password { get; set; }
+
+ public string TenantId { get; set; }
+
+ public string TenantUserName { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Owner.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Owner.cs
new file mode 100644
index 00000000..d549facd
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Owner.cs
@@ -0,0 +1,11 @@
+namespace Saas.LandingSignup.Web.Models
+{
+ public class Owner
+ {
+ public string Id { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string Email { get; set; }
+ public string ProfilePic { get; set; }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Tenant.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Tenant.cs
new file mode 100644
index 00000000..68c1a394
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Models/Tenant.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Saas.LandingSignup.Web.Models
+{
+ public class Tenant
+ {
+ public string Id { get; set; }
+ public Guid ApiIKey { get; set; }
+ public string TenantName { get; set; }
+ public bool IsActive { get; set; }
+ public string Name { get; set; }
+ public string DatabaseName { get; set; }
+ public string DatabaseServer { get; set; }
+ public string WebAppName { get; set; }
+ public string StorageContainerName { get; set; }
+ public string Url { get; set; }
+ public DateTime CreatedOn { get; set; }
+ public bool IsProvisioned { get; set; }
+ public bool IsCancelled { get; set; }
+ public int ProductId { get; set; }
+ public string ProvisioningStatus { get; set; }
+ public string ProductName { get; set; }
+ public string ProductTier { get; set; }
+ public int Category { get; set; }
+ public string CategoryName { get; set; }
+ public string Region { get; set; }
+ public string IpAddress { get; set; }
+ public int OrderId { get; set; }
+ public Owner Owner { get; set; }
+ public string UserName { get; set; }
+ public string OwnerEmail { get; set; }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Program.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Program.cs
new file mode 100644
index 00000000..a754e7c4
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Program.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace Saas.LandingSignup.Web
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/app-contoso047-dev-047 - Web Deploy/profile.arm.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/app-contoso047-dev-047 - Web Deploy/profile.arm.json
new file mode 100644
index 00000000..f516368e
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/app-contoso047-dev-047 - Web Deploy/profile.arm.json
@@ -0,0 +1,113 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_dependencyType": "appService.windows"
+ },
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "defaultValue": "rg-saas-dev-047",
+ "metadata": {
+ "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
+ }
+ },
+ "resourceGroupLocation": {
+ "type": "string",
+ "defaultValue": "eastus",
+ "metadata": {
+ "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
+ }
+ },
+ "resourceName": {
+ "type": "string",
+ "defaultValue": "app-contoso047-dev-047",
+ "metadata": {
+ "description": "Name of the main resource to be created by this template."
+ }
+ },
+ "resourceLocation": {
+ "type": "string",
+ "defaultValue": "[parameters('resourceGroupLocation')]",
+ "metadata": {
+ "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
+ }
+ }
+ },
+ "variables": {
+ "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+ "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('resourceGroupLocation')]",
+ "apiVersion": "2019-10-01"
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "apiVersion": "2019-10-01",
+ "dependsOn": [
+ "[parameters('resourceGroupName')]"
+ ],
+ "properties": {
+ "mode": "Incremental",
+ "template": {
+ "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "resources": [
+ {
+ "location": "[parameters('resourceLocation')]",
+ "name": "[parameters('resourceName')]",
+ "type": "Microsoft.Web/sites",
+ "apiVersion": "2015-08-01",
+ "tags": {
+ "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
+ },
+ "dependsOn": [
+ "[variables('appServicePlan_ResourceId')]"
+ ],
+ "kind": "app",
+ "properties": {
+ "name": "[parameters('resourceName')]",
+ "kind": "app",
+ "httpsOnly": true,
+ "reserved": false,
+ "serverFarmId": "[variables('appServicePlan_ResourceId')]",
+ "siteConfig": {
+ "metadata": [
+ {
+ "name": "CURRENT_STACK",
+ "value": "dotnetcore"
+ }
+ ]
+ }
+ },
+ "identity": {
+ "type": "SystemAssigned"
+ }
+ },
+ {
+ "location": "[parameters('resourceLocation')]",
+ "name": "[variables('appServicePlan_name')]",
+ "type": "Microsoft.Web/serverFarms",
+ "apiVersion": "2015-08-01",
+ "sku": {
+ "name": "S1",
+ "tier": "Standard",
+ "family": "S",
+ "size": "S1"
+ },
+ "properties": {
+ "name": "[variables('appServicePlan_name')]"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/app-org1-dev-026 - Web Deploy/profile.arm.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/app-org1-dev-026 - Web Deploy/profile.arm.json
new file mode 100644
index 00000000..2b3b569a
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/app-org1-dev-026 - Web Deploy/profile.arm.json
@@ -0,0 +1,113 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_dependencyType": "appService.windows"
+ },
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "defaultValue": "rg-saas-dev-026",
+ "metadata": {
+ "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
+ }
+ },
+ "resourceGroupLocation": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
+ }
+ },
+ "resourceName": {
+ "type": "string",
+ "defaultValue": "app-org1-dev-026",
+ "metadata": {
+ "description": "Name of the main resource to be created by this template."
+ }
+ },
+ "resourceLocation": {
+ "type": "string",
+ "defaultValue": "[parameters('resourceGroupLocation')]",
+ "metadata": {
+ "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
+ }
+ }
+ },
+ "variables": {
+ "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+ "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('resourceGroupLocation')]",
+ "apiVersion": "2019-10-01"
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "apiVersion": "2019-10-01",
+ "dependsOn": [
+ "[parameters('resourceGroupName')]"
+ ],
+ "properties": {
+ "mode": "Incremental",
+ "template": {
+ "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "resources": [
+ {
+ "location": "[parameters('resourceLocation')]",
+ "name": "[parameters('resourceName')]",
+ "type": "Microsoft.Web/sites",
+ "apiVersion": "2015-08-01",
+ "tags": {
+ "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
+ },
+ "dependsOn": [
+ "[variables('appServicePlan_ResourceId')]"
+ ],
+ "kind": "app",
+ "properties": {
+ "name": "[parameters('resourceName')]",
+ "kind": "app",
+ "httpsOnly": true,
+ "reserved": false,
+ "serverFarmId": "[variables('appServicePlan_ResourceId')]",
+ "siteConfig": {
+ "metadata": [
+ {
+ "name": "CURRENT_STACK",
+ "value": "dotnetcore"
+ }
+ ]
+ }
+ },
+ "identity": {
+ "type": "SystemAssigned"
+ }
+ },
+ {
+ "location": "[parameters('resourceLocation')]",
+ "name": "[variables('appServicePlan_name')]",
+ "type": "Microsoft.Web/serverFarms",
+ "apiVersion": "2015-08-01",
+ "sku": {
+ "name": "S1",
+ "tier": "Standard",
+ "family": "S",
+ "size": "S1"
+ },
+ "properties": {
+ "name": "[variables('appServicePlan_name')]"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/local/appInsights1.arm.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/local/appInsights1.arm.json
new file mode 100644
index 00000000..35b30d97
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/ServiceDependencies/local/appInsights1.arm.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "defaultValue": "rg-saas-dev-001",
+ "metadata": {
+ "_parameterType": "resourceGroup",
+ "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
+ }
+ },
+ "resourceGroupLocation": {
+ "type": "string",
+ "defaultValue": "eastus",
+ "metadata": {
+ "_parameterType": "location",
+ "description": "Location of the resource group. Resource groups could have different location than resources."
+ }
+ },
+ "resourceLocation": {
+ "type": "string",
+ "defaultValue": "[parameters('resourceGroupLocation')]",
+ "metadata": {
+ "_parameterType": "location",
+ "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('resourceGroupLocation')]",
+ "apiVersion": "2019-10-01"
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('app-provider-dev-001', subscription().subscriptionId)))]",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "apiVersion": "2019-10-01",
+ "dependsOn": [
+ "[parameters('resourceGroupName')]"
+ ],
+ "properties": {
+ "mode": "Incremental",
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "resources": [
+ {
+ "name": "app-provider-dev-001",
+ "type": "microsoft.insights/components",
+ "location": "[parameters('resourceLocation')]",
+ "kind": "web",
+ "properties": {},
+ "apiVersion": "2015-05-01"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "_dependencyType": "appInsights.azure"
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/launchSettings.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/launchSettings.json
new file mode 100644
index 00000000..bc6dcf27
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/launchSettings.json
@@ -0,0 +1,30 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:7560",
+ "sslPort": 44310
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
+ }
+ },
+ "Saas.LandingSignup.Web": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
+ },
+ "dotnetRunMessages": "true",
+ "applicationUrl": "https://localhost:5001;http://localhost:5000"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/serviceDependencies.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/serviceDependencies.json
new file mode 100644
index 00000000..f6a8923d
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/serviceDependencies.json
@@ -0,0 +1,23 @@
+{
+ "dependencies": {
+ "secrets1": {
+ "type": "secrets"
+ },
+ "appInsights1": {
+ "type": "appInsights",
+ "connectionId": "APPINSIGHTS_CONNECTIONSTRING"
+ },
+ "mssql1": {
+ "type": "mssql",
+ "connectionId": "ConnectionStrings:DefaultConnection"
+ },
+ "mssql2": {
+ "type": "mssql",
+ "connectionId": "ConnectionStrings:CatalogDbConnection"
+ },
+ "cosmosdb1": {
+ "type": "cosmosdb",
+ "connectionId": "ConnectionStrings:CosmosDb"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/serviceDependencies.local.json b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/serviceDependencies.local.json
new file mode 100644
index 00000000..1a524e1f
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Properties/serviceDependencies.local.json
@@ -0,0 +1,13 @@
+{
+ "dependencies": {
+ "secrets1": {
+ "type": "secrets.user"
+ },
+ "appInsights1": {
+ "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/microsoft.insights/components/app-provider-dev-001",
+ "type": "appInsights.azure",
+ "connectionId": "APPINSIGHTS_CONNECTIONSTRING",
+ "secretStore": "LocalSecretsFile"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Repositories/CustomerRepository.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Repositories/CustomerRepository.cs
new file mode 100644
index 00000000..6072123d
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Repositories/CustomerRepository.cs
@@ -0,0 +1,60 @@
+using Microsoft.Extensions.Configuration;
+using Saas.LandingSignup.Web.Models;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.SqlClient;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Repositories
+{
+ public class CustomerRepository
+ {
+ private readonly IConfiguration _configuration;
+
+ public CustomerRepository(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public async Task> GetAllCustomers(string tenantId)
+ {
+ try
+ {
+ List customers = new List();
+
+ using (var connection = new SqlConnection(_configuration["ConnectionStrings:CatalogDbConnection"]))
+ {
+ await connection.OpenAsync();
+ using (var command = new SqlCommand("SELECT * FROM dbo.Customer Where TenantId = @TenantId", connection))
+ {
+ SqlParameter param = new SqlParameter();
+ param.ParameterName = "@TenantId";
+ param.Value = tenantId;
+ command.Parameters.Add(param);
+
+ var reader = command.ExecuteReader();
+ while (reader.Read())
+ {
+ Customer customer = new Customer();
+ customer.Id = Guid.Parse(reader["Id"].ToString());
+ customer.TenantId = Guid.Parse(reader["TenantId"].ToString());
+ customer.CustomerName = reader["CustomerName"].ToString();
+ customer.IsActive = bool.Parse(reader["IsActive"].ToString());
+ customers.Add(customer);
+ }
+
+ if (!reader.IsClosed) await reader.CloseAsync();
+ }
+
+ if (connection.State != ConnectionState.Closed) await connection.CloseAsync();
+ return customers;
+ }
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Repositories/TenantRepository.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Repositories/TenantRepository.cs
new file mode 100644
index 00000000..aeaed17d
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Repositories/TenantRepository.cs
@@ -0,0 +1,48 @@
+using Microsoft.Extensions.Configuration;
+using System;
+using System.Data;
+using System.Data.SqlClient;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Repositories
+{
+ public class TenantRepository
+ {
+ private readonly IConfiguration _configuration;
+
+ public TenantRepository(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public async Task GetTenantId(Guid apiKey)
+ {
+ string tenantId = null;
+ try
+ {
+ using (var connection = new SqlConnection(_configuration["ConnectionStrings:CatalogDbConnection"]))
+ {
+ await connection.OpenAsync();
+ using (var command = new SqlCommand("SELECT Id FROM Tenant WHERE ApiKey = @apiKey", connection))
+ {
+ command.Parameters.AddWithValue("@apiKey", apiKey);
+ var reader = await command.ExecuteReaderAsync();
+ if (reader.Read())
+ {
+ tenantId = reader["Id"].ToString();
+ }
+
+ if (!reader.IsClosed) await reader.CloseAsync();
+ if (connection.State != ConnectionState.Closed) await connection.CloseAsync();
+
+ return tenantId;
+ }
+ }
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Saas.LandingSignup.Web.csproj b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Saas.LandingSignup.Web.csproj
new file mode 100644
index 00000000..eb5d7abb
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Saas.LandingSignup.Web.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net6.0
+ false
+ 7b599cf5-3102-4740-ab34-69dd240f9ea3
+ /subscriptions/357c83c2-bed7-4fe7-af6a-95835c6e2d91/resourceGroups/rg-saas-dev-001/providers/microsoft.insights/components/app-provider-dev-001
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/AzureAd.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/AzureAd.cs
new file mode 100644
index 00000000..8a9ab0dc
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/AzureAd.cs
@@ -0,0 +1,41 @@
+using Microsoft.Identity.Client;
+using Saas.LandingSignup.Web.Models;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Services
+{
+ public class AzureAd
+ {
+ private readonly AppSettings _appSettings;
+
+ private const string AuthorityFormat = "https://login.microsoftonline.com/{0}/v2.0";
+
+ const string MSATenantId = "";
+ public static string clientId = "";
+ public static string clientSecret = "";
+
+ private const string OnboardingScope = "";
+
+ public AzureAd(AppSettings appSettings)
+ {
+ _appSettings = appSettings;
+ }
+
+ public async Task GetAccessTokenAsync()
+ {
+ // Get a token for the Microsoft Graph. If this line throws an exception for any reason, we'll just let the exception be returned as a 500 response
+ // to the caller, and show a generic error message to the user.
+ IConfidentialClientApplication daemonClient;
+ daemonClient = ConfidentialClientApplicationBuilder.Create(clientId)
+ .WithAuthority(string.Format(AuthorityFormat, MSATenantId))
+ .WithRedirectUri(_appSettings.RedirectUri)
+ .WithClientSecret(clientSecret)
+ .Build();
+
+ AuthenticationResult authResult = await daemonClient.AcquireTokenForClient(new[] { OnboardingScope })
+ .ExecuteAsync();
+
+ return authResult;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/CosmosDbService.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/CosmosDbService.cs
new file mode 100644
index 00000000..0ca7caab
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/CosmosDbService.cs
@@ -0,0 +1,64 @@
+using Microsoft.Azure.Cosmos;
+using Saas.LandingSignup.Web.Models.CosmosDb;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Services
+{
+ public class CosmosDbService : ICosmosDbService
+ {
+ private Container _container;
+
+ public CosmosDbService(
+ CosmosClient dbClient,
+ string databaseName,
+ string containerName)
+ {
+ this._container = dbClient.GetContainer(databaseName, containerName);
+ }
+
+ public async Task AddItemAsync(Item item)
+ {
+ await this._container.CreateItemAsync- (item, new PartitionKey(item.Name));
+ }
+
+ public async Task DeleteItemAsync(string id)
+ {
+ await this._container.DeleteItemAsync
- (id, new PartitionKey(id));
+ }
+
+ public async Task
- GetItemAsync(string id)
+ {
+ try
+ {
+ ItemResponse
- response = await this._container.ReadItemAsync
- (id, new PartitionKey(id));
+ return response.Resource;
+ }
+ catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+
+ }
+
+ public async Task> GetItemsAsync(string queryString)
+ {
+ var query = this._container.GetItemQueryIterator
- (new QueryDefinition(queryString));
+ List
- results = new List
- ();
+ while (query.HasMoreResults)
+ {
+ var response = await query.ReadNextAsync();
+
+ results.AddRange(response.ToList());
+ }
+
+ return results;
+ }
+
+ public async Task UpdateItemAsync(string id, Item item)
+ {
+ await this._container.UpsertItemAsync
- (item, new PartitionKey(item.Name));
+ }
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/ICosmosDbService.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/ICosmosDbService.cs
new file mode 100644
index 00000000..1b5eba36
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/ICosmosDbService.cs
@@ -0,0 +1,15 @@
+using Saas.LandingSignup.Web.Models.CosmosDb;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Saas.LandingSignup.Web.Services
+{
+ public interface ICosmosDbService
+ {
+ Task> GetItemsAsync(string query);
+ Task
- GetItemAsync(string id);
+ Task AddItemAsync(Item item);
+ Task UpdateItemAsync(string id, Item item);
+ Task DeleteItemAsync(string id);
+ }
+}
diff --git a/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/Onboarding.cs b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/Onboarding.cs
new file mode 100644
index 00000000..bf9ffba4
--- /dev/null
+++ b/src/Saas.LandingSignup/Saas.LandingSignup.Web/Services/Onboarding.cs
@@ -0,0 +1,613 @@
+//----------------------
+//
+// Generated using the NSwag toolchain v (http://NSwag.org)
+//
+//----------------------
+
+#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended."
+#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword."
+#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?'
+#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ...
+#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..."
+#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'"
+#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant"
+
+namespace Saas.LandingSignup.Web.Services
+{
+ using System = global::System;
+
+ [System.CodeDom.Compiler.GeneratedCode("NSwag", "")]
+ public partial class OnboardingClient
+ {
+ private string _baseUrl = "";
+ private System.Net.Http.HttpClient _httpClient;
+ private System.Lazy _settings;
+
+ public OnboardingClient(string baseUrl, System.Net.Http.HttpClient httpClient)
+ {
+ BaseUrl = baseUrl;
+ _httpClient = httpClient;
+ _settings = new System.Lazy(CreateSerializerSettings);
+ }
+
+ private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
+ {
+ var settings = new Newtonsoft.Json.JsonSerializerSettings();
+ UpdateJsonSerializerSettings(settings);
+ return settings;
+ }
+
+ public string BaseUrl
+ {
+ get { return _baseUrl; }
+ set { _baseUrl = value; }
+ }
+
+ protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }
+
+ partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);
+
+ partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
+ partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
+ partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
+
+ /// Success
+ /// A server side error occurred.
+ public System.Threading.Tasks.Task> TenantsAllAsync()
+ {
+ return TenantsAllAsync(System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public async System.Threading.Tasks.Task> TenantsAllAsync(System.Threading.CancellationToken cancellationToken)
+ {
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Tenants");
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ request_.Method = new System.Net.Http.HttpMethod("GET");
+ request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false);
+ if (objectResponse_.Object == null)
+ {
+ throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
+ }
+ return objectResponse_.Object;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
+ /// Success
+ /// A server side error occurred.
+ public System.Threading.Tasks.Task TenantsPOSTAsync(Tenant body)
+ {
+ return TenantsPOSTAsync(body, System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public async System.Threading.Tasks.Task TenantsPOSTAsync(Tenant body, System.Threading.CancellationToken cancellationToken)
+ {
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Tenants");
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value));
+ content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
+ request_.Content = content_;
+ request_.Method = new System.Net.Http.HttpMethod("POST");
+ request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200 || status_ == 201)
+ {
+ var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false);
+ if (objectResponse_.Object == null)
+ {
+ throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
+ }
+ return objectResponse_.Object;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
+ /// Success
+ /// A server side error occurred.
+ public System.Threading.Tasks.Task TenantsGETAsync(System.Guid id)
+ {
+ return TenantsGETAsync(id, System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public async System.Threading.Tasks.Task TenantsGETAsync(System.Guid id, System.Threading.CancellationToken cancellationToken)
+ {
+ if (id == null)
+ throw new System.ArgumentNullException("id");
+
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Tenants/{id}");
+ urlBuilder_.Replace("{id}", System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture)));
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ request_.Method = new System.Net.Http.HttpMethod("GET");
+ request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false);
+ if (objectResponse_.Object == null)
+ {
+ throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
+ }
+ return objectResponse_.Object;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
+ /// Success
+ /// A server side error occurred.
+ public System.Threading.Tasks.Task TenantsPUTAsync(System.Guid id, Tenant body)
+ {
+ return TenantsPUTAsync(id, body, System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public async System.Threading.Tasks.Task TenantsPUTAsync(System.Guid id, Tenant body, System.Threading.CancellationToken cancellationToken)
+ {
+ if (id == null)
+ throw new System.ArgumentNullException("id");
+
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Tenants/{id}");
+ urlBuilder_.Replace("{id}", System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture)));
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value));
+ content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
+ request_.Content = content_;
+ request_.Method = new System.Net.Http.HttpMethod("PUT");
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ return;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
+ /// Success
+ /// A server side error occurred.
+ public System.Threading.Tasks.Task TenantsDELETEAsync(System.Guid id)
+ {
+ return TenantsDELETEAsync(id, System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public async System.Threading.Tasks.Task TenantsDELETEAsync(System.Guid id, System.Threading.CancellationToken cancellationToken)
+ {
+ if (id == null)
+ throw new System.ArgumentNullException("id");
+
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Tenants/{id}");
+ urlBuilder_.Replace("{id}", System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture)));
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ request_.Method = new System.Net.Http.HttpMethod("DELETE");
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ return;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
+ protected struct ObjectResponseResult
+ {
+ public ObjectResponseResult(T responseObject, string responseText)
+ {
+ this.Object = responseObject;
+ this.Text = responseText;
+ }
+
+ public T Object { get; }
+
+ public string Text { get; }
+ }
+
+ public bool ReadResponseAsString { get; set; }
+
+ protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken)
+ {
+ if (response == null || response.Content == null)
+ {
+ return new ObjectResponseResult(default(T), string.Empty);
+ }
+
+ if (ReadResponseAsString)
+ {
+ var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ try
+ {
+ var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText, JsonSerializerSettings);
+ return new ObjectResponseResult(typedBody, responseText);
+ }
+ catch (Newtonsoft.Json.JsonException exception)
+ {
+ var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
+ throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception);
+ }
+ }
+ else
+ {
+ try
+ {
+ using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
+ using (var streamReader = new System.IO.StreamReader(responseStream))
+ using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader))
+ {
+ var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings);
+ var typedBody = serializer.Deserialize(jsonTextReader);
+ return new ObjectResponseResult(typedBody, string.Empty);
+ }
+ }
+ catch (Newtonsoft.Json.JsonException exception)
+ {
+ var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
+ throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
+ }
+ }
+ }
+
+ private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
+ {
+ if (value == null)
+ {
+ return "";
+ }
+
+ if (value is System.Enum)
+ {
+ var name = System.Enum.GetName(value.GetType(), value);
+ if (name != null)
+ {
+ var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
+ if (field != null)
+ {
+ var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute))
+ as System.Runtime.Serialization.EnumMemberAttribute;
+ if (attribute != null)
+ {
+ return attribute.Value != null ? attribute.Value : name;
+ }
+ }
+
+ var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo));
+ return converted == null ? string.Empty : converted;
+ }
+ }
+ else if (value is bool)
+ {
+ return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant();
+ }
+ else if (value is byte[])
+ {
+ return System.Convert.ToBase64String((byte[])value);
+ }
+ else if (value.GetType().IsArray)
+ {
+ var array = System.Linq.Enumerable.OfType