Creation of new solution for Saas.LandingSignup to replace Saas.Provider. Updated all references to match new namespace.

This commit is contained in:
beshaghyspur 2022-02-15 11:53:57 -08:00
Родитель 3dbee8e112
Коммит c719e0f2f8
270 изменённых файлов: 137828 добавлений и 70 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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')]" },

Просмотреть файл

@ -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
<img src="https://stsaasprod001.blob.core.windows.net/assets/images/saas-provider-onboarding-flow.png">
## 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.

Просмотреть файл

@ -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") })
}
}

Просмотреть файл

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputPath>bin\$(Configuration)\</OutputPath>
<DebugSymbols>false</DebugSymbols>
<SkipCopyBuildProduct>true</SkipCopyBuildProduct>
<AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>
<TargetRuntime>None</TargetRuntime>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>
<BaseIntermediateOutputPath Condition=" !HasTrailingSlash('$(BaseIntermediateOutputPath)') ">$(BaseIntermediateOutputPath)\</BaseIntermediateOutputPath>
<IntermediateOutputPath>$(BaseIntermediateOutputPath)$(Configuration)\</IntermediateOutputPath>
<ProjectReferencesOutputPath Condition=" '$(ProjectReferencesOutputPath)' == '' ">$(IntermediateOutputPath)ProjectReferences</ProjectReferencesOutputPath>
<ProjectReferencesOutputPath Condition=" !HasTrailingSlash('$(ProjectReferencesOutputPath)') ">$(ProjectReferencesOutputPath)\</ProjectReferencesOutputPath>
<StageArtifacts Condition=" '$(StageArtifacts)' == '' ">true</StageArtifacts>
</PropertyGroup>
<PropertyGroup>
<DefineCommonItemSchemas>false</DefineCommonItemSchemas>
<DefineCommonCapabilities>false</DefineCommonCapabilities>
</PropertyGroup>
<ProjectExtensions>
<ProjectCapabilities>
<DeploymentProject />
</ProjectCapabilities>
</ProjectExtensions>
<ItemDefinitionGroup>
<Content>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<ProjectReference>
<Private>false</Private>
<Targets>Build</Targets>
</ProjectReference>
</ItemDefinitionGroup>
<Target Name="CreateManifestResourceNames" />
<PropertyGroup>
<StageArtifactsDependsOn>
_GetDeploymentProjectContent;
_CalculateContentOutputRelativePaths;
_GetReferencedProjectsOutput;
_CalculateArtifactStagingDirectory;
_CopyOutputToArtifactStagingDirectory;
</StageArtifactsDependsOn>
</PropertyGroup>
<Target Name="_CopyOutputToArtifactStagingDirectory">
<Copy SourceFiles="@(DeploymentProjectContentOutput)" DestinationFiles="$(ArtifactStagingDirectory)\$(MSBuildProjectName)%(RelativePath)" />
<Copy SourceFiles="@(BuildProjectReferencesOutput)" DestinationFiles="$(ArtifactStagingDirectory)\$(MSBuildProjectName)\%(ProjectName)\%(RecursiveDir)%(FileName)%(Extension)" />
</Target>
<Target Name="_GetDeploymentProjectContent">
<MSBuild Projects="$(MSBuildProjectFile)" Targets="ContentFilesProjectOutputGroup">
<Output TaskParameter="TargetOutputs" ItemName="DeploymentProjectContentOutput" />
</MSBuild>
</Target>
<Target Name="_GetReferencedProjectsOutput">
<PropertyGroup>
<MsBuildProperties>Configuration=$(Configuration);Platform=$(Platform)</MsBuildProperties>
</PropertyGroup>
<MSBuild Projects="@(ProjectReference)"
BuildInParallel="$(BuildInParallel)"
Properties="$(MsBuildProperties)"
Targets="%(ProjectReference.Targets)" />
<ItemGroup>
<BuildProjectReferencesOutput Include="%(ProjectReference.IncludeFilePath)">
<ProjectName>$([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)'))</ProjectName>
</BuildProjectReferencesOutput>
</ItemGroup>
</Target>
<Target Name="_CalculateArtifactStagingDirectory" Condition=" '$(ArtifactStagingDirectory)'=='' ">
<PropertyGroup>
<ArtifactStagingDirectory Condition=" '$(OutDir)'!='' ">$(OutDir)</ArtifactStagingDirectory>
<ArtifactStagingDirectory Condition=" '$(ArtifactStagingDirectory)'=='' ">$(OutputPath)</ArtifactStagingDirectory>
<ArtifactStagingDirectory Condition=" !HasTrailingSlash('$(ArtifactStagingDirectory)') ">$(ArtifactStagingDirectory)\</ArtifactStagingDirectory>
<ArtifactStagingDirectory>$(ArtifactStagingDirectory)staging\</ArtifactStagingDirectory>
<ArtifactStagingDirectory Condition=" '$(Build_StagingDirectory)'!='' AND '$(TF_Build)'=='True' ">$(Build_StagingDirectory)</ArtifactStagingDirectory>
</PropertyGroup>
</Target>
<!-- Appends each of the deployment project's content output files with metadata indicating its relative path from the deployment project's folder. -->
<Target Name="_CalculateContentOutputRelativePaths"
Outputs="%(DeploymentProjectContentOutput.Identity)">
<PropertyGroup>
<_OriginalIdentity>%(DeploymentProjectContentOutput.Identity)</_OriginalIdentity>
<_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', ''))</_RelativePath>
</PropertyGroup>
<ItemGroup>
<DeploymentProjectContentOutput>
<RelativePath>$(_RelativePath)</RelativePath>
</DeploymentProjectContentOutput>
</ItemGroup>
</Target>
<Target Name="CoreCompile" />
<PropertyGroup>
<StageArtifactsAfterTargets Condition=" '$(StageArtifacts)' == 'true' ">
PrepareForRun
</StageArtifactsAfterTargets>
</PropertyGroup>
<Target Name="StageArtifacts" DependsOnTargets="$(StageArtifactsDependsOn)" AfterTargets="$(StageArtifactsAfterTargets)"/>
<!-- Custom target to clean up local deployment staging files -->
<Target Name="DeleteBinObjFolders" BeforeTargets="Clean">
<RemoveDir Directories="$(OutputPath)" />
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
</Target>
</Project>

Просмотреть файл

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|AnyCPU">
<Configuration>Debug</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|AnyCPU">
<Configuration>Release</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>6a0836ed-483e-4f7e-8248-ac00fb52e778</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<PrepareForBuildDependsOn>
</PrepareForBuildDependsOn>
</PropertyGroup>
<Import Condition=" Exists('Deployment.targets') " Project="Deployment.targets" />
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />
<!-- vertag<:>start tokens<:>maj.min -->
<Import Condition=" Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Deployment\1.1\DeploymentProject.targets') " Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Deployment\1.1\DeploymentProject.targets" />
<!-- vertag<:>end -->
<ItemGroup>
<Content Include="azuredeploy.json" />
<Content Include="azuredeploy.parameters.json" />
<None Include="Deployment.targets">
<Visible>False</Visible>
</None>
<Content Include="Deploy-AzureResourceGroup.ps1" />
</ItemGroup>
<Target Name="GetReferenceAssemblyPaths" />
</Project>

Просмотреть файл

@ -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": {
}
}

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

@ -0,0 +1,5 @@
{
"version": 1,
"isRoot": true,
"tools": {}
}

Просмотреть файл

@ -0,0 +1,3 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -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<CreateController> _logger;
private readonly AppSettings _appSettings;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ICosmosDbService _cosmosDbService;
public CreateController(ILogger<CreateController> logger, IOptions<AppSettings> appSettings, UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> 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<IActionResult> 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<Category> categories = new List<Category>();
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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

Просмотреть файл

@ -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<List<Customer>> 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);
}
}
}
}

Просмотреть файл

@ -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<HomeController> _logger;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ICosmosDbService _cosmosDbService;
public HomeController(ILogger<HomeController> logger, UserManager<ApplicationUser> userManager, ICosmosDbService cosmosDbService)
{
_logger = logger;
_userManager = userManager;
_cosmosDbService = cosmosDbService;
}
public IActionResult Help()
{
return View();
}
public IActionResult Index()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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 });
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace Saas.LandingSignup.Web.Controllers
{
public class RequestHeaderController : Controller
{
public IActionResult Index()
{
return View();
}
}
}

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -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<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
}

Просмотреть файл

@ -0,0 +1,277 @@
// <auto-generated />
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<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasColumnType("nvarchar(256)")
.HasMaxLength(256);
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasColumnType("nvarchar(256)")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasColumnType("nvarchar(256)")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasColumnType("nvarchar(256)")
.HasMaxLength(256);
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(128)")
.HasMaxLength(128);
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(128)")
.HasMaxLength(128);
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(128)")
.HasMaxLength(128);
b.Property<string>("Name")
.HasColumnType("nvarchar(128)")
.HasMaxLength(128);
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

Просмотреть файл

@ -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<string>(nullable: false),
Name = table.Column<string>(maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(nullable: false),
UserName = table.Column<string>(maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
Email = table.Column<string>(maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(nullable: false),
PasswordHash = table.Column<string>(nullable: true),
SecurityStamp = table.Column<string>(nullable: true),
ConcurrencyStamp = table.Column<string>(nullable: true),
PhoneNumber = table.Column<string>(nullable: true),
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
TwoFactorEnabled = table.Column<bool>(nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
LockoutEnabled = table.Column<bool>(nullable: false),
AccessFailedCount = table.Column<int>(nullable: false),
TenantId = table.Column<string>(maxLength: 37, nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
RoleId = table.Column<string>(nullable: false),
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(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<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
UserId = table.Column<string>(nullable: false),
ClaimType = table.Column<string>(nullable: true),
ClaimValue = table.Column<string>(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<string>(maxLength: 128, nullable: false),
ProviderKey = table.Column<string>(maxLength: 128, nullable: false),
ProviderDisplayName = table.Column<string>(nullable: true),
UserId = table.Column<string>(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<string>(nullable: false),
RoleId = table.Column<string>(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<string>(nullable: false),
LoginProvider = table.Column<string>(maxLength: 128, nullable: false),
Name = table.Column<string>(maxLength: 128, nullable: false),
Value = table.Column<string>(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");
}
}
}

Просмотреть файл

@ -0,0 +1,280 @@
// <auto-generated />
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<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Saas.LandingSignup.Web.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<string>("TenantId")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

Просмотреть файл

@ -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<string>(
name: "TenantId",
table: "AspNetUsers",
type: "nvarchar(37)",
nullable: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TenantId",
table: "AspNetUsers");
}
}
}

Просмотреть файл

@ -0,0 +1,278 @@
// <auto-generated />
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<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Saas.LandingSignup.Web.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<string>("TenantId")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("Saas.LandingSignup.Web.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Identity;
namespace Saas.LandingSignup.Web.Models
{
public class ApplicationUser : IdentityUser
{
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -0,0 +1,9 @@
namespace Saas.LandingSignup.Web.Models
{
public class ErrorViewModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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<Startup>();
});
}
}

Просмотреть файл

@ -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')]"
}
}
]
}
}
}
]
}

Просмотреть файл

@ -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')]"
}
}
]
}
}
}
]
}

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

@ -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<List<Customer>> GetAllCustomers(string tenantId)
{
try
{
List<Customer> customers = new List<Customer>();
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;
}
}
}
}

Просмотреть файл

@ -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<string> 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;
}
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<UserSecretsId>7b599cf5-3102-4740-ab34-69dd240f9ea3</UserSecretsId>
<ApplicationInsightsResourceId>/subscriptions/357c83c2-bed7-4fe7-af6a-95835c6e2d91/resourceGroups/rg-saas-dev-001/providers/microsoft.insights/components/app-provider-dev-001</ApplicationInsightsResourceId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.20.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.2" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.24.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Identity.Client" Version="4.41.0" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\assets\images\favicon\" />
</ItemGroup>
</Project>

Просмотреть файл

@ -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<AuthenticationResult> 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;
}
}
}

Просмотреть файл

@ -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>(item, new PartitionKey(item.Name));
}
public async Task DeleteItemAsync(string id)
{
await this._container.DeleteItemAsync<Item>(id, new PartitionKey(id));
}
public async Task<Item> GetItemAsync(string id)
{
try
{
ItemResponse<Item> response = await this._container.ReadItemAsync<Item>(id, new PartitionKey(id));
return response.Resource;
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
public async Task<IEnumerable<Item>> GetItemsAsync(string queryString)
{
var query = this._container.GetItemQueryIterator<Item>(new QueryDefinition(queryString));
List<Item> results = new List<Item>();
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>(item, new PartitionKey(item.Name));
}
}
}

Просмотреть файл

@ -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<IEnumerable<Item>> GetItemsAsync(string query);
Task<Item> GetItemAsync(string id);
Task AddItemAsync(Item item);
Task UpdateItemAsync(string id, Item item);
Task DeleteItemAsync(string id);
}
}

Просмотреть файл

@ -0,0 +1,613 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v (http://NSwag.org)
// </auto-generated>
//----------------------
#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<Newtonsoft.Json.JsonSerializerSettings> _settings;
public OnboardingClient(string baseUrl, System.Net.Http.HttpClient httpClient)
{
BaseUrl = baseUrl;
_httpClient = httpClient;
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(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);
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task<System.Collections.Generic.ICollection<Tenant>> TenantsAllAsync()
{
return TenantsAllAsync(System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<Tenant>> 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<System.Collections.Generic.ICollection<Tenant>>(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();
}
}
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task<Tenant> TenantsPOSTAsync(Tenant body)
{
return TenantsPOSTAsync(body, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task<Tenant> 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<Tenant>(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();
}
}
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task<Tenant> TenantsGETAsync(System.Guid id)
{
return TenantsGETAsync(id, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task<Tenant> 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<Tenant>(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();
}
}
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task TenantsPUTAsync(System.Guid id, Tenant body)
{
return TenantsPUTAsync(id, body, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
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();
}
}
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task TenantsDELETEAsync(System.Guid id)
{
return TenantsDELETEAsync(id, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
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<T>
{
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<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Threading.CancellationToken cancellationToken)
{
if (response == null || response.Content == null)
{
return new ObjectResponseResult<T>(default(T), string.Empty);
}
if (ReadResponseAsString)
{
var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
try
{
var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseText, JsonSerializerSettings);
return new ObjectResponseResult<T>(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<T>(jsonTextReader);
return new ObjectResponseResult<T>(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<object>((System.Array)value);
return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo)));
}
var result = System.Convert.ToString(value, cultureInfo);
return result == null ? "" : result;
}
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.6.3.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Tenant
{
[Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Guid Id { get; set; }
[Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string Name { get; set; }
[Newtonsoft.Json.JsonProperty("isActive", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public bool? IsActive { get; set; }
[Newtonsoft.Json.JsonProperty("isCancelled", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public bool IsCancelled { get; set; }
[Newtonsoft.Json.JsonProperty("isProvisioned", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public bool IsProvisioned { get; set; }
[Newtonsoft.Json.JsonProperty("apiKey", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Guid ApiKey { get; set; }
[Newtonsoft.Json.JsonProperty("categoryId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public int CategoryId { get; set; }
[Newtonsoft.Json.JsonProperty("productId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public int ProductId { get; set; }
[Newtonsoft.Json.JsonProperty("userId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string UserId { get; set; }
[Newtonsoft.Json.JsonProperty("created", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.DateTimeOffset Created { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "")]
public partial class ApiException : System.Exception
{
public int StatusCode { get; private set; }
public string Response { get; private set; }
public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }
public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Exception innerException)
: base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException)
{
StatusCode = statusCode;
Response = response;
Headers = headers;
}
public override string ToString()
{
return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString());
}
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "")]
public partial class ApiException<TResult> : ApiException
{
public TResult Result { get; private set; }
public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result, System.Exception innerException)
: base(message, statusCode, response, headers, innerException)
{
Result = result;
}
}
}
#pragma warning restore 1591
#pragma warning restore 1573
#pragma warning restore 472
#pragma warning restore 114
#pragma warning restore 108
#pragma warning restore 3016

Просмотреть файл

@ -0,0 +1,120 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Saas.LandingSignup.Web.Data;
using Saas.LandingSignup.Web.Models;
using Saas.LandingSignup.Web.Repositories;
using Saas.LandingSignup.Web.Services;
using System;
using System.Threading.Tasks;
namespace Saas.LandingSignup.Web
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("IdentityDbConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<ApplicationDbContext>();
services.Configure<IdentityOptions>(options =>
{
// Default SignIn settings.
options.SignIn.RequireConfirmedEmail = false;
options.SignIn.RequireConfirmedPhoneNumber = false;
// Default Password settings.
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 0;
});
services.AddControllersWithViews();
services.AddScoped<TenantRepository, TenantRepository>();
services.AddScoped<CustomerRepository, CustomerRepository>();
var appSettings = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettings);
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(1);
});
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_CONNECTIONSTRING"]);
services.AddSingleton<ICosmosDbService>(InitializeCosmosClientInstanceAsync(Configuration.GetSection("AppSettings:CosmosDb")).GetAwaiter().GetResult());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
var routes = endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
if (env.IsDevelopment())
{
routes.WithMetadata(new AllowAnonymousAttribute());
}
endpoints.MapRazorPages();
});
}
/// <summary>
/// Creates a Cosmos DB database and a container with the specified partition key.
/// </summary>
/// <returns></returns>
private static async Task<CosmosDbService> InitializeCosmosClientInstanceAsync(IConfigurationSection configurationSection)
{
string databaseName = configurationSection.GetSection("DatabaseName").Value;
string containerName = configurationSection.GetSection("ContainerName").Value;
string account = configurationSection.GetSection("Account").Value;
string key = configurationSection.GetSection("Key").Value;
Microsoft.Azure.Cosmos.CosmosClient client = new Microsoft.Azure.Cosmos.CosmosClient(account, key);
CosmosDbService cosmosDbService = new CosmosDbService(client, databaseName, containerName);
Microsoft.Azure.Cosmos.DatabaseResponse database = await client.CreateDatabaseIfNotExistsAsync(databaseName);
await database.Database.CreateContainerIfNotExistsAsync(containerName, "/name");
return cosmosDbService;
}
}
}

Просмотреть файл

@ -0,0 +1,8 @@
namespace Saas.LandingSignup.Web.ViewModels
{
public class ConfirmationViewModel
{
public Models.Tenant Tenant { get; set; }
public Models.OnboardingFlow OnboardingFlow { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,10 @@
@{
ViewBag.Title = "Dashboard";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<script type="text/javascript">
$(document).ready(function () {
$('.dashboard').addClass('active');
});
</script>

Просмотреть файл

@ -0,0 +1,10 @@
@{
ViewBag.Title = "Manage";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<script type="text/javascript">
$(document).ready(function () {
$('.manage').addClass('active');
});
</script>

Просмотреть файл

@ -0,0 +1,10 @@
@{
ViewBag.Title = "Users";
Layout = "~/Views/Shared/_LayoutAdmin.cshtml";
}
<script type="text/javascript">
$(document).ready(function () {
$('.users').addClass('active');
});
</script>

Просмотреть файл

@ -0,0 +1,44 @@
@model IEnumerable<Saas.LandingSignup.Web.Models.Category>
@{
ViewData["Title"] = "Create your Subscriber - Select a Category";
}
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12">
<div style="font-size:.75rem;">STEP 2 OF 3</div>
<div style="margin-top:.5rem;"><progress class="progress" id="file" value="66" max="100"> 66% </progress></div>
</div>
</div>
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12" style="margin-top: 1rem;">
<div style="margin-top: .5rem;">
<div style="margin-bottom: 10px;">
<h3 style="margin-bottom: 0px;">In what industry is your organization?</h3>
</div>
<div>
<p>Identifying your industry will help people find you in search results. Choose the closest one - you can update it later.</p>
</div>
<form method="post">
<div style="margin-bottom: 30px;">
<select class="form-control" name="CategoryId" required>
<option value="">Select an Industry</option>
@foreach (var item in Model)
{
<option value="@item.Id">
@item.Name
</option>
}
</select>
</div>
<div style="margin-top: .5rem;">
<input type="hidden" name="Id" value="@ViewBag.Id" />
<input type="hidden" name="UserId" value="@ViewBag.UserId" />
<input type="hidden" name="IsExistingUser" value="@ViewBag.IsExistingUser" />
<input type="hidden" name="Name" value="@ViewBag.Name" />
<input type="submit" class="btn btn-primary" style="width:100%;" value="NEXT" />
</div>
</form>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,29 @@
@{
ViewData["Title"] = "Create your Subscriber - Your Subscriber has been created!";
}
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12">
<div style="font-size:.75rem;">COMPLETED</div>
<div style="margin-top:.5rem;"><progress class="progress" id="file" value="100" max="100"></progress></div>
</div>
</div>
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12" style="margin-top: 1.5rem;">
<div style="color: green;">
<div style="float: left; margin-right: 15px;">
<i class="fas fa-check-circle" style="font-size: 2.5rem;"></i>
</div>
<div>
<h2>You're all set!</h2>
</div>
</div>
<div style="margin-top: 15px;">
<p>
Your Organization has been created!
</p>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,5 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}

Просмотреть файл

@ -0,0 +1,47 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12">
<div style="flex-wrap: nowrap;">
<div class="col-4" style="padding-left: 0px;">
<div class="number-circle" style="margin: 0 auto; text-align: center; margin-bottom: 1rem;">1</div>
<div class="progress-bar" style="width: 100%; height: .25rem; background-color: #51a852;"></div>
<div style="text-align: center;">Name</div>
</div>
<div class="col-4" style="padding-left: 5px; padding-right: 5px;">
<div class="number-circle" style="margin: 0 auto; text-align: center; margin-bottom: 1rem; background: #cccccc;">2</div>
<div class="progress-bar" style="width: 100%; height: .25rem; background-color: #cccccc;"></div>
<div style="text-align: center;">Category</div>
</div>
<div class="col-4" style="padding-right: 0px;">
<div class="number-circle" style="margin: 0 auto; text-align: center; margin-bottom: 1rem; background: #cccccc;">3</div>
<div class="progress-bar" style="width: 100%; height: .25rem; background-color: #cccccc;"></div>
<div style="text-align: center;">Complete</div>
</div>
</div>
<div style="margin-top: 1rem;">
<div style="margin-top: .5rem;">
<div style="margin-bottom: 10px;">
<h3 style="margin-bottom: 0px;">What do you want to name this Subscriber?</h3>
</div>
<form method="post">
<div style="margin-top: 15px; margin-bottom: 1rem;">
<input type="text" name="name" class="form-control" placeholder="Start typing..." />
</div>
<div>
<p>The Subscriber name should be the name of your business, personal brand or organization. You can change this later.</p>
</div>
<div style="margin-top: 1rem;">
<input type="submit" class="btn btn-primary" style="width:100%;" value="Next" />
</div>
</form>
</div>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,47 @@
<link href="../../css/create.css" rel="stylesheet" />
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12" style="margin-top:1.5rem;">
<script src="https://js.stripe.com/v3"></script>
<input type="submit" id="checkout-button" style="visibility:hidden;" />
</div>
</div>
<script type="text/javascript">
var price;
// If statement to manage price plan
if (@ViewBag.ProductId == 6)
{
price = "@ViewBag.StripeProductPlanSubscriberBasic";
}
else if (@ViewBag.ProductId == 7)
{
price = "@ViewBag.StripeProductPlanSubscriberStandard";
}
var stripe = Stripe('@ViewBag.StripePublishableKey');
var checkoutButton = document.querySelector('#checkout-button');
checkoutButton.addEventListener('click', function () {
stripe.redirectToCheckout({
lineItems: [{
// Define the product and price in the Dashboard first, and use the price
// ID in your client-side code. You may also pass a SKU id into the `price`
// field
price: price,
quantity: 1
}],
mode: 'subscription',
successUrl: 'https://@Context.Request.Host/create/deploy?id=@ViewBag.Id&userId=@ViewBag.UserId&isExistingUser=@ViewBag.IsExistingUser&name=@ViewBag.Name&categoryId=@ViewBag.CategoryId&productId=@ViewBag.ProductId',
cancelUrl: 'https://@Context.Request.Host/create/plans?id=@ViewBag.Id&userId=@ViewBag.UserId&isExistingUser=@ViewBag.IsExistingUser&name=@ViewBag.Name&categoryId=@ViewBag.CategoryId&productId=@ViewBag.ProductId'
});
});
</script>
<script type="text/javascript">
$(function () {
$('#checkout-button').click();
});
</script>

Просмотреть файл

@ -0,0 +1,42 @@
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor
@{
ViewData["Title"] = "Create your Subscriber - Give your Subscriber a Name";
}
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12">
<div style="font-size:.75rem;">STEP 1 OF 3</div>
<div style="margin-top:.5rem;"><progress class="progress" id="file" value="33" max="100"> 33% </progress></div>
</div>
</div>
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12" style="margin-top: 1.5rem;">
<div style="margin-bottom: 10px;">
<h3 style="margin-bottom: 0px;">What is your Organization's Name?</h3>
</div>
<form method="post">
<div style="margin-top: 1rem; margin-bottom: 1rem;">
<input type="text" name="name" class="form-control" placeholder="Start typing..." required minlength="2" maxlength="44" pattern="[a-zA-Z0-9_' ]*$" value="@Context.Request.Query["name"]" />
</div>
@if (Context.Request.Query["userNameExists"] == "True")
{
<div>
<p style="color:#dc3545!important;"> Subscriber name '@Context.Request.Query["name"]' is already taken.</p>
</div>
}
<div>
<p>The name should be the name of your business, brand or organization. You can change this later.</p>
</div>
<div style="margin-top: 1rem;">
<input type="hidden" name="Id" value="@ViewBag.Id" />
<input type="hidden" name="UserId" value="@ViewBag.UserId" />
<input type="hidden" name="IsExistingUser" value="@ViewBag.IsExistingUser" />
<input type="submit" class="btn btn-primary" style="width:100%;" value="NEXT" />
</div>
</form>
</div>
</div>

Просмотреть файл

@ -0,0 +1,148 @@
@model Saas.LandingSignup.Web.Models.Tenant
@{
ViewData["Title"] = "Create your Subscriber - Select a Plan";
}
<style type="text/css">
.form .plan input, .form .payment-plan input, .form .payment-type input {
display: none;
}
.form label {
position: relative;
color: #fff;
background-color: #8ecda9;
font-size: 1rem;
text-align: center;
height: 100px;
line-height: 50px;
display: block;
cursor: pointer;
border: 3px solid transparent;
border-radius: 5px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.form .plan input:checked + label, .form .payment-plan input:checked + label, .form .payment-type input:checked + label {
border: 2px solid #51a852;
/**/ background-color: #51a852;
}
.form .plan input:checked + label:after, form .payment-plan input:checked + label:after, .form .payment-type input:checked + label:after {
content: "\2713";
font-weight: 700;
font-size: 1.5rem;
width: 40px;
height: 40px;
line-height: 40px;
border-radius: 100%;
border: 2px solid #fff;
background-color: #51a852;
z-index: 999;
position: absolute;
top: -10px;
right: -10px;
}
</style>
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12">
<div style="font-size:.75rem;">STEP 3 OF 3</div>
<div style="margin-top:.5rem;"><progress class="progress" id="file" value="90" max="100"></progress></div>
</div>
</div>
<form class="form" method="post" onsubmit="displayOverlay(this);">
<div class="row" style="margin: 0 auto; max-width: 600px;">
<div class="col-12" style="margin-top: 1rem;">
<div>
<h5>Choose the plan that's right for you</h5>
Upgrade or downgrade at any time.
</div>
<div class="row plan" style="margin-top:1rem;">
<div class="col-4">
<input type="radio" name="ProductId" id="free" value="5" checked><label class="free-label" for="free"><span style="font-size:2rem;"><i class="fas fa-user"></i></span><br />Free</label>
</div>
<div class="col-4">
<input type="radio" name="ProductId" id="basic" value="6"><label class="basic-label" for="basic"><span style="font-size:2rem;"><i class="fas fa-user-friends"></i></span><br />Basic</label>
</div>
<div class="col-4">
<input type="radio" name="ProductId" id="standard" value="7"><label class="standard-label" for="standard"><span style="font-size:2rem;"><i class="fas fa-users"></i></span><br />Standard</label>
</div>
</div>
<div style="margin-top:1rem; text-align:center; font-weight:700;">Free Trial</div>
<div class="row" style="margin-top:1rem; text-align:center;">
<div class="col-4">
Free Forever
</div>
<div class="col-4">
7 Days
</div>
<div class="col-4">
14 Days
</div>
</div>
<hr />
<div style="margin-top:1rem; text-align:center; font-weight:700;">Monthly price</div>
<div class="row" style="margin-top:1rem; text-align:center;">
<div class="col-4">
$0
</div>
<div class="col-4">
$4.99
</div>
<div class="col-4">
$9.99
</div>
</div>
<hr />
<div style="margin-top:1rem; text-align:center; font-weight:700;">Ad Supported</div>
<div class="row" style="margin: 0 auto; max-width: 600px; margin-top:1rem; text-align:center;">
<div class="col-4">
Yes
</div>
<div class="col-4">
Minimal
</div>
<div class="col-4">
None
</div>
</div>
<hr />
<div style="margin-top: 2rem;">
<input type="hidden" name="Id" value="@ViewBag.Id" />
<input type="hidden" name="UserId" value="@ViewBag.UserId" />
<input type="hidden" name="IsExistingUser" value="@ViewBag.IsExistingUser" />
<input type="hidden" name="Name" value="@ViewBag.Name" />
<input type="hidden" name="CategoryId" value="@ViewBag.CategoryId" />
<input type="submit" id="submitButton" class="btn btn-primary" style="width:100%;" value="NEXT">
</div>
</div>
</div>
</form>
<!-- Display Processing Overlay -->
<script type="text/javascript">
var displayOverlay = function (form) {
// Disable submit button
$('input[type=submit]').prop('disabled', true); // Disables visually + functionally
// Set submit button value
document.getElementById('submitButton').value = 'CREATING YOUR TENANT...';
// Display overlay
$("<div />").css({
'position': 'fixed',
'left': 0,
'right': 0,
'bottom': 0,
'top': 0,
'background': 'black',
'opacity': '.5',
'z-index': '1000',
'text-align': 'center'
}).appendTo($("body")).append($("<div style='margin-top:20rem; color:#ffffff; font-weight:500'><i class='fas fa-circle-notch fa-spin' style='font-size:3rem;'></i><br><br>Creating your Tenant</div>"));
}
</script>

Просмотреть файл

@ -0,0 +1,5 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}

Просмотреть файл

@ -0,0 +1,181 @@
@{
ViewData["Title"] = "SaaS Service | Your SaaS Service";
Layout = "~/Views/Shared/_LayoutMarketing.cshtml";
}
<!-- ======= Jumbotron Section ======= -->
<div class="container">
<div class="bg-white rounded-lg">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">Welcome to your SaaS Service</h1>
<p class="col-md-12 fs-4" style="padding-left:0px;">Software as a service (SaaS) allows users to connect to and use cloud-based apps over the Internet. Common examples are email, calendaring, and office tools (such as Microsoft Office 365).</p>
<form method="post">
<div>
<input type="email" name="emailAddress" class="form-control" style="width:100%; max-width:350px;" placeholder="Enter your Email Address" required />
</div>
<div style="margin-top:.75rem;">
<input type="submit" class="btn btn-cta btn-primary" value="GET STARTED FREE" style="width:100%; max-width:350px;" />
</div>
</form>
</div>
</div>
</div>
<!-- End Jumbotron Section -->
<!-- ======= Features Section ======= -->
<section id="services" class="services section-bg">
<div class="container">
<div class="section-title">
<h2 style="color:#ffffff;">The Modern SaaS Platform</h2>
<p style="color:#ffffff;">Get your SaaS solution up and running on Microsoft Azure</p>
</div>
<div class="row">
<div class="col-md-6 mt-4">
<div class="icon-box">
<i class="fas fa-project-diagram"></i>
<h4>Easy Onboarding</h4>
<p>Multitenant architecture is a perfect choice for applications that need an easy onboarding experience</p>
</div>
</div>
<div class="col-md-6 mt-4">
<div class="icon-box">
<i class="fas fa-bolt"></i>
<h4>Maintenance</h4>
<p>Improved ease of maintenance is a benefit of multitenant applications</p>
</div>
</div>
<div class="col-md-6 mt-4">
<div class="icon-box">
<i class="fas fa-user-check"></i>
<h4>Reduced Cost</h4>
<p>Multitenant architecture is relatively inexpensive due to shared infrastructure and and lower maintenance costs</p>
</div>
</div>
<div class="col-md-6 mt-4">
<div class="icon-box">
<i class="fas fa-globe"></i>
<h4>Resource usage</h4>
<p>Multitenant architecture ensures better use of the available resources compared to single-tenant architecture</p>
</div>
</div>
<div class="col-md-6 mt-4">
<div class="icon-box">
<i class="fas fa-times-circle"></i>
<h4>Easier Updates</h4>
<p>In a multitenant application, upgrades are seamless and relatively simple</p>
</div>
</div>
<div class="col-md-6 mt-4">
<div class="icon-box">
<i class="fab fa-connectdevelop"></i>
<h4>Isolation of tenant data</h4>
<p>Tenant data in a multitenant application is logically and physically isolated on a per-tenant basis</p>
</div>
</div>
</div>
<div class="row" style="margin-top:2rem;">
<div class="col-md-12" style="text-align:center;">
<a href="#" class="btn btn-cta btn-primary" style="width:100%; max-width:350px;">Get Started Free</a>
</div>
</div>
</div>
</section>
<!-- End Features Section -->
<!-- ======= Pricing Section ======= -->
<section class="pricing py-5">
<div class="container">
<div class="section-title">
<h2>Subscription Pricing</h2>
<p>Startups, ISVs and Enterprises can provide multiple Service Tiers</p>
</div>
<div class="row">
<!-- Free Tier -->
<div class="col-md-3">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Free</h5>
<h6 class="card-price text-center">$0<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span>Single User</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>5GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Unlimited Private Projects</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Dedicated Phone Support</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Free Subdomain</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up For Free</a>
</div>
</div>
</div>
<!-- Basic Tier -->
<div class="col-md-3">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Basic</h5>
<h6 class="card-price text-center">$4.99<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>5 Users</strong></li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>50GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Private Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Dedicated Phone Support</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Free Subdomain</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up</a>
</div>
</div>
</div>
<!-- Standard Tier -->
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Standard</h5>
<h6 class="card-price text-center">$9.99<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited Users</strong></li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>150GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Private Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Dedicated Phone Support</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited</strong> Free Subdomains</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up</a>
</div>
</div>
</div>
<!-- Premium Tier -->
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Premium</h5>
<h6 class="card-price text-center">$19.99<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited Users</strong></li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>150GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Private Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Dedicated Phone Support</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited</strong> Free Subdomains</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up</a>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- End Features Section -->

Просмотреть файл

@ -0,0 +1,100 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<section class="pricing py-5">
<div class="container">
<div class="section-title">
<h2>Subscription Pricing</h2>
<p>Startups, ISVs and Enterprises can provide multiple Service Tiers</p>
</div>
<div class="row">
<!-- Free Tier -->
<div class="col-md-3">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Free</h5>
<h6 class="card-price text-center">$0<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span>Single User</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>5GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Unlimited Private Projects</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Dedicated Phone Support</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Free Subdomain</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up For Free</a>
</div>
</div>
</div>
<!-- Basic Tier -->
<div class="col-md-3">
<div class="card mb-5 mb-lg-0">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Basic</h5>
<h6 class="card-price text-center">$4.99<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>5 Users</strong></li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>50GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Private Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Dedicated Phone Support</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Free Subdomain</li>
<li class="text-muted"><span class="fa-li"><i class="fas fa-times"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up</a>
</div>
</div>
</div>
<!-- Standard Tier -->
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Standard</h5>
<h6 class="card-price text-center">$9.99<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited Users</strong></li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>150GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Private Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Dedicated Phone Support</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited</strong> Free Subdomains</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up</a>
</div>
</div>
</div>
<!-- Premium Tier -->
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title text-muted text-uppercase text-center">Premium</h5>
<h6 class="card-price text-center">$19.99<span class="period">/month</span></h6>
<hr>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited Users</strong></li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>150GB Storage</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Public Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Community Access</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Unlimited Private Projects</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Dedicated Phone Support</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span><strong>Unlimited</strong> Free Subdomains</li>
<li><span class="fa-li"><i class="fas fa-check"></i></span>Monthly Status Reports</li>
</ul>
<a href="#" class="btn btn-block btn-primary text-uppercase">Sign Up</a>
</div>
</div>
</div>
</div>
</div>
</section>

Просмотреть файл

@ -0,0 +1,6 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

Просмотреть файл

@ -0,0 +1,7 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
@ViewBag.Tenant

Просмотреть файл

@ -0,0 +1,25 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

Просмотреть файл

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css?v=1" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.1/css/all.css">
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index"><i class="fas fa-users"></i>&nbsp;&nbsp;SaaS Service</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Pricing">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Help">Help</a>
</li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
<i class="fab fa-microsoft"></i> Microsoft - &copy; 2021 - Azure SaaS - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Просмотреть файл

@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css?v=1.2" />
<!-- Custom fonts for this template-->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.1/css/all.css">
<link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i" rel="stylesheet">
<!-- Custom styles for this template-->
<link href="~/css/admin.css?v=1.3" rel="stylesheet">
<script src="~/vendor/jquery/jquery.min.js"></script>
<!-- Page level plugins -->
<script src="~/vendor/chart.js/Chart.min.js"></script>
<!-- Favicon -->
<link rel="icon" type="image/png" href="~/assets/images/favicon/16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="~/assets/images/favicon/32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="~/assets/images/favicon/48x48.png" sizes="48x48">
<link rel="icon" type="image/png" href="~/assets/images/favicon/64x64.png" sizes="64x64">
@RenderSection("OpenGraphRender", false)
<style type="text/css">
.navbar-light .navbar-toggler {
border-color: #ffffff;
}
.btn {
padding: .6rem 1rem;
}
</style>
</head>
<body id="page-top">
<nav class="navbar navbar-expand-sm navbar-light bg-white topbar static-top shadow">
<div style="width:100%">
<div style="float:left;">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index"><i class="fas fa-cogs"></i>&nbsp;<span style="font-weight:500">Tenant Administration</span></a>
</div>
<div style="float:right;">
<partial name="_LoginPartial" />
</div>
</div>
</nav>
<div class="d-flex" id="wrapper">
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar" style="background-color:#243a5e" role="navigation">
@*Nav Item - Dashboard*@
<li class="nav-item dashboard">
<a class="nav-link" href="dashboard">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span>
</a>
</li>
@*Divider*@
<hr class="sidebar-divider">
<li class="nav-item manage">
<a class="nav-link" href="manage">
<i class="fas fa-layer-group"></i>
<span>Manage</span>
</a>
</li>
<li class="nav-item users">
<a class="nav-link" href="users">
<i class="fas fa-users"></i>
<span>Users</span>
</a>
</li>
@*Divider*@
<hr class="sidebar-divider d-none d-md-block">
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
<div class="sidebar-card">
<p class="text-center mb-2"><strong>SaaS Services</strong> is packed with premium features, components, and more!</p>
<a class="btn btn-success btn-sm" href="upgrade">Upgrade!</a>
</div>
</ul>
<!-- End of Sidebar -->
<!-- Page Content -->
<div id="page-content-wrapper">
<div class="container" style="margin-top:2rem;">
<main id="main" role="main" style="margin-bottom:2rem;" class="main">
@RenderBody()
</main>
</div>
</div>
<!-- /#page-content-wrapper -->
<!-- Footer Content -->
<footer class="border-top footer text-muted">
<div class="container">
<i class="fab fa-microsoft"></i> Microsoft - &copy; 2021 - Azure SaaS - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<!-- /footer-content -->
</div>
<!-- /#wrapper -->
@RenderSection("Scripts", required: false)
<!-- Bootstrap core JavaScript-->
<script src="~/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="~/vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="~/js/admin.js"></script>
<!-- Page level plugins -->
<script src="~/vendor/chart.js/Chart.min.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - SaaS Service</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css?v=1" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.1/css/all.css">
<!-- Favicon -->
<link rel="icon" type="image/png" href="~/assets/images/favicon/16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="~/assets/images/favicon/32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="~/assets/images/favicon/48x48.png" sizes="48x48">
<link rel="icon" type="image/png" href="~/assets/images/favicon/64x64.png" sizes="64x64">
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index"><i class="fas fa-users"></i>&nbsp;&nbsp;SaaS Service</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Pricing">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Resources" asp-action="Index">Resources</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Help">Help</a>
</li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>
</header>
<main role="main" class="pb-3">
@RenderBody()
</main>
<footer class="border-top footer text-muted">
<div class="container">
<i class="fab fa-microsoft"></i> Microsoft - &copy; 2021 - Azure SaaS - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Просмотреть файл

@ -0,0 +1,27 @@
@using Microsoft.AspNetCore.Identity
@using Saas.LandingSignup.Web.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
</li>
}
</ul>

Просмотреть файл

@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

Просмотреть файл

@ -0,0 +1,3 @@
@using Saas.LandingSignup.Web
@using Saas.LandingSignup.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Просмотреть файл

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

Просмотреть файл

@ -0,0 +1,35 @@
{
"AppSettings": {
"RedirectUri": "",
"CosmosDb": {
"Account": "",
"Key": "",
"DatabaseName": "",
"ContainerName": ""
},
"OnboardingApiBaseUrl": "",
"SendGridAPIKey": "",
"Stripe": {
"PublishableKey": "",
"SecretKey": "",
"ProductPlanSubscriberBasic": "",
"ProductPlanSubscriberStandard": ""
}
},
"ConnectionStrings": {
"DefaultConnection": "",
"CatalogDbConnection": "",
"CosmosDbConnection": ""
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ApplicationInsights": {
"ConnectionString": ""
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

Ширина:  |  Высота:  |  Размер: 8.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.7 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.4 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.3 KiB

Просмотреть файл

@ -0,0 +1,641 @@
#wrapper {
overflow-x: hidden;
}
.sidebar {
width: 6.5rem;
min-height: 100vh;
margin-right: 1.5rem;
display: none
}
.sidebar .nav-item {
position: relative;
}
.sidebar .nav-item:last-child {
margin-bottom: 1rem;
}
.sidebar .nav-item .nav-link {
text-align: center;
padding: 0.75rem 1rem;
width: 6.5rem;
}
.sidebar .nav-item .nav-link span {
font-size: 0.65rem;
display: block;
}
.sidebar .nav-item.active .nav-link {
font-weight: 700;
}
.sidebar .nav-item .collapse {
position: absolute;
left: calc(6.5rem + 1.5rem / 2);
z-index: 1;
top: 2px;
}
.sidebar .nav-item .collapse .collapse-inner {
border-radius: 0.35rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
}
.sidebar .nav-item .collapsing {
display: none;
transition: none;
}
.sidebar .nav-item .collapse .collapse-inner,
.sidebar .nav-item .collapsing .collapse-inner {
padding: .5rem 0;
min-width: 10rem;
font-size: 0.85rem;
margin: 0 0 1rem 0;
}
.sidebar .nav-item .collapse .collapse-inner .collapse-header,
.sidebar .nav-item .collapsing .collapse-inner .collapse-header {
margin: 0;
white-space: nowrap;
padding: .5rem 1.5rem;
text-transform: uppercase;
font-weight: 800;
font-size: 0.65rem;
color: #b7b9cc;
}
.sidebar .nav-item .collapse .collapse-inner .collapse-item,
.sidebar .nav-item .collapsing .collapse-inner .collapse-item {
padding: 0.5rem 1rem;
margin: 0 0.5rem;
display: block;
color: #3a3b45;
text-decoration: none;
border-radius: 0.35rem;
white-space: nowrap;
}
.sidebar .nav-item .collapse .collapse-inner .collapse-item:hover,
.sidebar .nav-item .collapsing .collapse-inner .collapse-item:hover {
background-color: #eaecf4;
}
.sidebar .nav-item .collapse .collapse-inner .collapse-item:active,
.sidebar .nav-item .collapsing .collapse-inner .collapse-item:active {
background-color: #dddfeb;
}
.sidebar .nav-item .collapse .collapse-inner .collapse-item.active,
.sidebar .nav-item .collapsing .collapse-inner .collapse-item.active {
color: #4e73df;
font-weight: 700;
}
.sidebar #sidebarToggle {
width: 2.5rem;
height: 2.5rem;
text-align: center;
margin-bottom: 1rem;
cursor: pointer;
}
.sidebar #sidebarToggle::after {
font-weight: 900;
content: '\f104';
font-family: 'Font Awesome 5 Free';
margin-right: 0.1rem;
}
.sidebar #sidebarToggle:hover {
text-decoration: none;
}
.sidebar #sidebarToggle:focus {
outline: none;
}
.sidebar.toggled {
width: 0 !important;
overflow: hidden;
}
.sidebar.toggled #sidebarToggle::after {
content: '\f105';
font-family: 'Font Awesome 5 Free';
margin-left: 0.25rem;
}
.sidebar.toggled .sidebar-card {
display: none;
}
.sidebar .sidebar-brand {
height: 4.375rem;
text-decoration: none;
font-size: 1rem;
font-weight: 800;
padding: 1.5rem 1rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05rem;
z-index: 1;
}
.sidebar .sidebar-brand .sidebar-brand-icon i {
font-size: 2rem;
}
.sidebar .sidebar-brand .sidebar-brand-text {
display: none;
}
.sidebar hr.sidebar-divider {
margin: 0 1rem 1rem;
}
.sidebar .sidebar-heading {
text-align: center;
padding: 0 1rem;
font-weight: 800;
font-size: 0.65rem;
}
.sidebar .sidebar-card {
display: flex;
flex-direction: column;
align-items: center;
font-size: 0.875rem;
border-radius: 0.35rem;
color: rgba(255, 255, 255, 0.8);
margin-left: 1rem;
margin-right: 1rem;
margin-bottom: 1rem;
padding: 1rem;
background-color: rgba(0, 0, 0, 0.1);
}
.sidebar .sidebar-card .sidebar-card-illustration {
height: 3rem;
display: block;
}
.sidebar .sidebar-card .sidebar-card-title {
font-weight: bold;
}
.sidebar .sidebar-card p {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
@media (min-width: 768px) {
.sidebar {
width: 14rem !important;
display: inherit;
}
.sidebar .nav-item .collapse {
position: relative;
left: 0;
z-index: 1;
top: 0;
-webkit-animation: none;
animation: none;
}
.sidebar .nav-item .collapse .collapse-inner {
border-radius: 0;
box-shadow: none;
}
.sidebar .nav-item .collapsing {
display: block;
transition: height 0.15s ease;
}
.sidebar .nav-item .collapse,
.sidebar .nav-item .collapsing {
margin: 0 1rem;
}
.sidebar .nav-item .nav-link {
display: block;
width: 100%;
text-align: left;
padding: 1rem;
width: 14rem;
}
.sidebar .nav-item .nav-link i {
font-size: 0.85rem;
margin-right: 0.25rem;
}
.sidebar .nav-item .nav-link span {
font-size: 0.85rem;
display: inline;
}
.sidebar .nav-item .nav-link[data-toggle="collapse"]::after {
width: 1rem;
text-align: center;
float: right;
vertical-align: 0;
border: 0;
font-weight: 900;
content: '\f107';
font-family: 'Font Awesome 5 Free';
}
.sidebar .nav-item .nav-link[data-toggle="collapse"].collapsed::after {
content: '\f105';
}
.sidebar .sidebar-brand .sidebar-brand-icon i {
font-size: 2rem;
}
.sidebar .sidebar-brand .sidebar-brand-text {
display: inline;
}
.sidebar .sidebar-heading {
text-align: left;
}
.sidebar.toggled {
overflow: visible;
width: 6.5rem !important;
}
.sidebar.toggled .nav-item .collapse {
position: absolute;
left: calc(6.5rem + 1.5rem / 2);
z-index: 1;
top: 2px;
-webkit-animation-name: growIn;
animation-name: growIn;
-webkit-animation-duration: 200ms;
animation-duration: 200ms;
-webkit-animation-timing-function: transform cubic-bezier(0.18, 1.25, 0.4, 1), opacity cubic-bezier(0, 1, 0.4, 1);
animation-timing-function: transform cubic-bezier(0.18, 1.25, 0.4, 1), opacity cubic-bezier(0, 1, 0.4, 1);
}
.sidebar.toggled .nav-item .collapse .collapse-inner {
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
border-radius: 0.35rem;
}
.sidebar.toggled .nav-item .collapsing {
display: none;
transition: none;
}
.sidebar.toggled .nav-item .collapse,
.sidebar.toggled .nav-item .collapsing {
margin: 0;
}
.sidebar.toggled .nav-item:last-child {
margin-bottom: 1rem;
}
.sidebar.toggled .nav-item .nav-link {
text-align: center;
padding: 0.75rem 1rem;
width: 6.5rem;
}
.sidebar.toggled .nav-item .nav-link span {
font-size: 0.65rem;
display: block;
}
.sidebar.toggled .nav-item .nav-link i {
margin-right: 0;
}
.sidebar.toggled .nav-item .nav-link[data-toggle="collapse"]::after {
display: none;
}
.sidebar.toggled .sidebar-brand .sidebar-brand-icon i {
font-size: 2rem;
}
.sidebar.toggled .sidebar-brand .sidebar-brand-text {
display: none;
}
.sidebar.toggled .sidebar-heading {
text-align: center;
}
}
.sidebar-light .sidebar-brand {
color: #6e707e;
}
.sidebar-light hr.sidebar-divider {
border-top: 1px solid #eaecf4;
}
.sidebar-light .sidebar-heading {
color: #b7b9cc;
}
.sidebar-light .nav-item .nav-link {
color: #858796;
}
.sidebar-light .nav-item .nav-link i {
color: #d1d3e2;
}
.sidebar-light .nav-item .nav-link:active, .sidebar-light .nav-item .nav-link:focus, .sidebar-light .nav-item .nav-link:hover {
color: #6e707e;
}
.sidebar-light .nav-item .nav-link:active i, .sidebar-light .nav-item .nav-link:focus i, .sidebar-light .nav-item .nav-link:hover i {
color: #6e707e;
}
.sidebar-light .nav-item .nav-link[data-toggle="collapse"]::after {
color: #b7b9cc;
}
.sidebar-light .nav-item.active .nav-link {
color: #6e707e;
}
.sidebar-light .nav-item.active .nav-link i {
color: #6e707e;
}
.sidebar-light #sidebarToggle {
background-color: #eaecf4;
}
.sidebar-light #sidebarToggle::after {
color: #b7b9cc;
}
.sidebar-light #sidebarToggle:hover {
background-color: #dddfeb;
}
.sidebar-dark .sidebar-brand {
color: #fff;
}
.sidebar-dark hr.sidebar-divider {
border-top: 1px solid rgba(255, 255, 255, 0.15);
}
.sidebar-dark .sidebar-heading {
color: rgba(255, 255, 255, 0.4);
}
.sidebar-dark .nav-item .nav-link {
color: rgba(255, 255, 255, 0.8);
}
.sidebar-dark .nav-item .nav-link i {
color: rgba(255, 255, 255, 0.3);
}
.sidebar-dark .nav-item .nav-link:active, .sidebar-dark .nav-item .nav-link:focus, .sidebar-dark .nav-item .nav-link:hover {
color: #fff;
}
.sidebar-dark .nav-item .nav-link:active i, .sidebar-dark .nav-item .nav-link:focus i, .sidebar-dark .nav-item .nav-link:hover i {
color: #fff;
}
.sidebar-dark .nav-item .nav-link[data-toggle="collapse"]::after {
color: rgba(255, 255, 255, 0.5);
}
.sidebar-dark .nav-item.active .nav-link {
color: #fff;
}
.sidebar-dark .nav-item.active .nav-link i {
color: #fff;
}
.sidebar-dark #sidebarToggle {
background-color: rgba(255, 255, 255, 0.2);
}
.sidebar-dark #sidebarToggle::after {
color: rgba(255, 255, 255, 0.5);
}
.sidebar-dark #sidebarToggle:hover {
background-color: rgba(255, 255, 255, 0.25);
}
.sidebar-dark.toggled #sidebarToggle::after {
color: rgba(255, 255, 255, 0.5);
}
#page-content-wrapper {
min-width: 100vw;
}
#title {
display: none;
}
#wrapper.toggled #accordionSidebar {
width: 10rem;
visibility: visible;
}
#wrapper.toggled .list-group {
width: 10rem;
visibility: visible;
}
#wrapper.toggled #title {
display: normal;
}
.list-group-item {
color: rgba(255,255,255,.8);
background-color: #000000;
padding: .75rem;
font-size: .9rem;
}
.list-group-item:hover {
background-color: #000000;
color: #ffffff;
}
.list-group-item:focus {
background-color: #000000;
color: #ffffff;
}
@media (min-width: 768px) {
/* For desktop: */
#sidebar-wrapper {
visibility: visible;
margin-left: 0;
}
#page-content-wrapper {
min-width: 0;
width: 100%;
}
#wrapper.toggled #sidebar-wrapper {
}
#wrapper.toggled #title {
display: initial;
}
}
/* End Simple Sidebar */
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-bottom-primary {
border-bottom: 0.25rem solid #4e73df !important;
}
.border-left-secondary {
border-left: 0.25rem solid #858796 !important;
}
.border-bottom-secondary {
border-bottom: 0.25rem solid #858796 !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-bottom-success {
border-bottom: 0.25rem solid #1cc88a !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
.border-bottom-info {
border-bottom: 0.25rem solid #36b9cc !important;
}
.border-left-warning {
border-left: 0.25rem solid #f6c23e !important;
}
.border-bottom-warning {
border-bottom: 0.25rem solid #f6c23e !important;
}
.border-left-danger {
border-left: 0.25rem solid #e74a3b !important;
}
.border-bottom-danger {
border-bottom: 0.25rem solid #e74a3b !important;
}
.border-left-light {
border-left: 0.25rem solid #f8f9fc !important;
}
.border-bottom-light {
border-bottom: 0.25rem solid #f8f9fc !important;
}
.border-left-dark {
border-left: 0.25rem solid #5a5c69 !important;
}
.border-bottom-dark {
border-bottom: 0.25rem solid #5a5c69 !important;
}
.card .card-header .dropdown {
line-height: 1;
}
.card .card-header .dropdown .dropdown-menu {
line-height: 1.5;
}
.card .card-header[data-toggle="collapse"] {
text-decoration: none;
position: relative;
padding: 0.75rem 3.25rem 0.75rem 1.25rem;
}
.card .card-header[data-toggle="collapse"]::after {
position: absolute;
right: 0;
top: 0;
padding-right: 1.725rem;
line-height: 51px;
font-weight: 900;
content: '\f107';
font-family: 'Font Awesome 5 Free';
color: #d1d3e2;
}
.card .card-header[data-toggle="collapse"].collapsed {
border-radius: 0.35rem;
}
.card .card-header[data-toggle="collapse"].collapsed::after {
content: '\f105';
}
.chart-area {
position: relative;
height: 10rem;
width: 100%;
}
@media (min-width: 768px) {
.chart-area {
height: 20rem;
}
}
.chart-bar {
position: relative;
height: 10rem;
width: 100%;
}
@media (min-width: 768px) {
.chart-bar {
height: 20rem;
}
}
.chart-pie {
position: relative;
height: 15rem;
width: 100%;
}
@media (min-width: 768px) {
.chart-pie {
height: calc(20rem - 43px) !important;
}
}

Просмотреть файл

@ -0,0 +1,239 @@
/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
/* Provide sufficient contrast against white background */
a {
color: #0366d6;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
/* Sticky footer styles
-------------------------------------------------- */
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}
body {
/* Margin bottom by footer height */
margin-bottom: 60px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px; /* Vertically center the text there */
}
/* Section styles
-------------------------------------------------- */
section {
padding: 60px 0;
}
.section-bg {
background-color: #243a5e;
}
.section-title {
text-align: center;
padding-bottom: 30px;
}
.section-title h2 {
font-size: 32px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 20px;
position: relative;
}
.section-title h2::before {
content: '';
position: absolute;
display: block;
width: 120px;
height: 1px;
background: #ddd;
bottom: 1px;
left: calc(50% - 60px);
}
.section-title h2::after {
content: '';
position: absolute;
display: block;
width: 40px;
height: 3px;
background: #3498db;
bottom: 0;
left: calc(50% - 20px);
}
.section-title p {
margin-bottom: 0;
}
/*--------------------------------------------------------------
# Services
--------------------------------------------------------------*/
.services .icon-box {
padding: 30px;
border-radius: 6px;
background: #fff;
transition: ease-in-out 0.3s;
}
.services .icon-box i {
float: left;
color: #3498db;
font-size: 40px;
}
.services .icon-box h4 {
margin-left: 70px;
font-weight: 700;
margin-bottom: 15px;
font-size: 18px;
}
.services .icon-box h4 a {
color: #384046;
transition: 0.3s;
}
.services .icon-box p {
margin-left: 70px;
line-height: 24px;
font-size: 14px;
}
.services .icon-box:hover {
box-shadow: 0px 2px 22px rgba(0, 0, 0, 0.08);
}
.services .icon-box:hover h4 a {
color: #3498db;
}
/* Pricing styles
-------------------------------------------------- */
.pricing .card {
border: none;
border-radius: 1rem;
transition: all 0.2s;
box-shadow: 0 0.5rem 1rem 0 rgba(0, 0, 0, 0.1);
}
.pricing hr {
margin: 1.5rem 0;
}
.pricing .card-title {
margin: 0.5rem 0;
font-size: 0.9rem;
letter-spacing: .1rem;
font-weight: bold;
}
.pricing .card-price {
font-size: 3rem;
margin: 0;
}
.pricing .card-price .period {
font-size: 0.8rem;
}
.pricing ul li {
margin-bottom: 1rem;
}
.pricing .text-muted {
opacity: 0.7;
}
.pricing .btn {
font-size: 80%;
border-radius: 5rem;
letter-spacing: .1rem;
font-weight: bold;
padding: 1rem;
opacity: 0.7;
transition: all 0.2s;
}
/* Hover Effects on Card */
@media (min-width: 992px) {
.pricing .card:hover {
margin-top: -.25rem;
margin-bottom: .25rem;
box-shadow: 0 0.5rem 1rem 0 rgba(0, 0, 0, 0.3);
}
.pricing .card:hover .btn {
opacity: 1;
}
}
/* Onboarding Progress */
.progress {
height: 30px;
border-radius: .25rem;
width: 100%;
}
progress[value]::-webkit-progress-bar {
background-color: #ededed;
border-radius: .25rem;
}
progress[value]::-webkit-progress-value {
border-radius: .25rem;
background-color: #51a852;
}

Двоичные данные
src/Saas.LandingSignup/Saas.LandingSignup.Web/wwwroot/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.3 KiB

Просмотреть файл

@ -0,0 +1,56 @@
(function($) {
"use strict"; // Start of use strict
// Toggle the side navigation
$("#sidebarToggle, #sidebarToggleTop").on('click', function(e) {
$("body").toggleClass("sidebar-toggled");
$(".sidebar").toggleClass("toggled");
if ($(".sidebar").hasClass("toggled")) {
$('.sidebar .collapse').collapse('hide');
};
});
// Close any open menu accordions when window is resized below 768px
$(window).resize(function() {
if ($(window).width() < 768) {
$('.sidebar .collapse').collapse('hide');
};
// Toggle the side navigation when window is resized below 480px
if ($(window).width() < 480 && !$(".sidebar").hasClass("toggled")) {
$("body").addClass("sidebar-toggled");
$(".sidebar").addClass("toggled");
$('.sidebar .collapse').collapse('hide');
};
});
// Prevent the content wrapper from scrolling when the fixed side navigation hovered over
$('body.fixed-nav .sidebar').on('mousewheel DOMMouseScroll wheel', function(e) {
if ($(window).width() > 768) {
var e0 = e.originalEvent,
delta = e0.wheelDelta || -e0.detail;
this.scrollTop += (delta < 0 ? 1 : -1) * 30;
e.preventDefault();
}
});
// Scroll to top button appear
$(document).on('scroll', function() {
var scrollDistance = $(this).scrollTop();
if (scrollDistance > 100) {
$('.scroll-to-top').fadeIn();
} else {
$('.scroll-to-top').fadeOut();
}
});
// Smooth scrolling using jQuery easing
$(document).on('click', 'a.scroll-to-top', function(e) {
var $anchor = $(this);
$('html, body').stop().animate({
scrollTop: ($($anchor.attr('href')).offset().top)
}, 1000, 'easeInOutExpo');
e.preventDefault();
});
})(jQuery); // End of use strict

Просмотреть файл

@ -0,0 +1,4 @@
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.

Просмотреть файл

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2011-2018 Twitter, Inc.
Copyright (c) 2011-2018 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,331 @@
/*!
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg {
overflow: hidden;
vertical-align: middle;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
select {
word-wrap: normal;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше