* Merge updated client
|
@ -1,19 +1,9 @@
|
|||
name: Hugo Docs Build
|
||||
name: Hugo Docs Build - GH Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- docs-updates
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
- '**.png'
|
||||
- '**.svg'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- docs-updates
|
||||
- main
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'docs/**'
|
||||
|
@ -22,38 +12,29 @@ on:
|
|||
- '**.svg'
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Deploy Job
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup Docsy
|
||||
run: git submodule update --init --recursive && sudo npm install -D --save autoprefixer && sudo npm install -D --save postcss-cli
|
||||
- name: Build And Deploy
|
||||
id: builddeploy
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GRAY_PEBBLE_0A877460F }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
|
||||
action: "upload"
|
||||
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
|
||||
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
|
||||
app_build_command: hugo
|
||||
app_location: "/docs/azure-saas-docs" # App source code path
|
||||
output_location: "public" # Built app content directory - optional
|
||||
###### End of Repository/Build Configurations ######
|
||||
submodules: recursive # Fetch the Docsy theme
|
||||
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
||||
|
||||
close_pull_request_job:
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
name: Close Pull Request Job
|
||||
steps:
|
||||
- name: Close Pull Request
|
||||
id: closepullrequest
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GRAY_PEBBLE_0A877460F }}
|
||||
action: "close"
|
||||
hugo-version: '0.91.2'
|
||||
extended: true
|
||||
|
||||
- name: Setup Docsy
|
||||
run: git submodule update --init --recursive && npm install -D --save autoprefixer && npm install -D --save postcss-cli && npm install -D --save postcss
|
||||
|
||||
- run: hugo
|
||||
working-directory: ./docs/azure-saas-docs
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/azure-saas-docs/public
|
|
@ -18,8 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApplication", "src\Saas.Authorization\DemoApplication\DemoApplication.csproj", "{1769DC80-92F9-4823-BC47-FF503FC1017C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.Application.Web", "src\Saas.Application\Saas.Application.Web\Saas.Application.Web.csproj", "{29BBEAD7-1043-41EC-A89D-766E1402B701}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saas.AspNetCore.Authorization", "src\Saas.Authorization\Saas.AspNetCore.Authorization\Saas.AspNetCore.Authorization.csproj", "{6B9F75FC-4739-4CA1-9347-2F4092A794B1}"
|
||||
|
@ -54,10 +52,6 @@ Global
|
|||
{B75AFCC1-A92D-4122-9FB2-6C478C67132F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B75AFCC1-A92D-4122-9FB2-6C478C67132F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B75AFCC1-A92D-4122-9FB2-6C478C67132F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1769DC80-92F9-4823-BC47-FF503FC1017C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1769DC80-92F9-4823-BC47-FF503FC1017C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1769DC80-92F9-4823-BC47-FF503FC1017C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1769DC80-92F9-4823-BC47-FF503FC1017C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{29BBEAD7-1043-41EC-A89D-766E1402B701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{29BBEAD7-1043-41EC-A89D-766E1402B701}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{29BBEAD7-1043-41EC-A89D-766E1402B701}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
|
50
README.md
|
@ -1,53 +1,8 @@
|
|||
# Azure SaaS Development Kit (ASDK)
|
||||
|
||||
[![.NET Onboarding](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-onboarding.yml/badge.svg)](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-onboarding.yml) [![.NET Identity](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-identity.yml/badge.svg)](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-identity.yml) [![.NET Provider](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-provider.yml/badge.svg)](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-provider.yml) [![.NET Admin](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-admin.yml/badge.svg)](https://github.com/Azure/azure-saas/actions/workflows/dotnet-saas-admin.yml)
|
||||
The Azure SaaS Development Kit (ASDK) provides a starting point into cloud-based Software as a Service (SaaS) for developers, startups, ISVs and enterprises. A platform for platform creators. The ASDK provides a reference architecture that is extensible to meet your application's needs, microservice-oriented, and fully-documented to empower all levels of experience in their entry into Azure Cloud Services.
|
||||
|
||||
The Azure SaaS Development Kit (ASDK) provides a reference architecture, deployable reference implementation and tools to help developers, startups, ISVs and Enterprises deliver their applications as a SaaS service. A platform for platform creators. (Public Preview)
|
||||
|
||||
[![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.Deployment%2FSaas.Deployment.Root%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fmain%2Fsrc%2FSaas.Deployment%2FSaas.Deployment.Root%2FcreateUiDefinition.json)
|
||||
|
||||
## Included in the Kit
|
||||
1. A multitenant reference architecture
|
||||
1. A sample reference implemenation that can be deployed in minutes
|
||||
1. Documentation with tips, tricks, and best practices related to onboarding new tenants, elasticity, operational architecture, billing, identity, security, monitoring and more
|
||||
1. Links to production SaaS platforms built using the Azure SaaS Development Kit
|
||||
|
||||
<!-- https://www.azuresaas.net -->
|
||||
|
||||
## Reference Architecture
|
||||
|
||||
<img src="docs/assets/images/azure-saas-multitenant-architecture.png" width="850">
|
||||
|
||||
## Reference Implementation
|
||||
The reference implementation provides an end-to-end SaaS service including all required microservices and their corresponding data stores to power your SaaS service. Simply [deploy](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fmain%2Fsrc%2FSaas.Deployment%2FSaas.Deployment.Root%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fmain%2Fsrc%2FSaas.Deployment%2FSaas.Deployment.Root%2FcreateUiDefinition.json) to your Azure subscription, clone the repo and migrate your business logic.
|
||||
|
||||
<!-- Demo SaaS Service: https://www.azuresaas.net -->
|
||||
|
||||
## Deployment
|
||||
[Deploy](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fmain%2Fsrc%2FSaas.Deployment%2FSaas.Deployment.Root%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fmain%2Fsrc%2FSaas.Deployment%2FSaas.Deployment.Root%2FcreateUiDefinition.json) an instance of the reference implemenation in less than 5 minutes. Once the deployment completes, you'll have all the resources deployed in your Azure subscription. Please be aware that while the costs are low, you are responsible for any charges incurred. Deploy the full service or deploy [microservices / componenents](docs/components.md) indivually.
|
||||
|
||||
<img src="docs/assets/images/azure-saas-multitenant-deployment.png" width="850">
|
||||
|
||||
## Production SaaS Reference
|
||||
|
||||
<a href="https://www.onsubscriber.com" target="_blank">Subscriber</a> is a live production SaaS solution from ISV Modern Appz built entirely on the Azure SaaS Development Kit.</p><p><a href="https://www.onsubscriber.com" target="_blank">Subscriber</a> allows users to easily build their Email and SMS lists using social login with Facebook, Google, Apple, Email and SMS. In addition, users can add profile pictures, bios, external links and social accounts to their tenants.
|
||||
|
||||
<a href="https://www.onsubscriber.com" target="_blank"><img src="docs/assets/images/azure-saas-production-service-subscriber.png" /></a>
|
||||
|
||||
## Solution Roadmap
|
||||
- Azure Kubernetes Services (AKS) for tenant containerization
|
||||
- Azure Container Registry (ACR)
|
||||
- Azure Bicep for Azure resource deployments
|
||||
- .NET MAUI Cross Platform Mobile Apps - Multitenant
|
||||
|
||||
## Subscribe for Updates
|
||||
Subscribe for email notifications of updates and new features:
|
||||
<a href="https://www.onsubscriber.com/azuresaas" target="_blank">https://www.onsubscriber.com/azuresaas</a>
|
||||
|
||||
## Downloads
|
||||
<a href="https://www.azuresaas.net/resources" target="_blank">https://www.azuresaas.net/resources</a>
|
||||
- How to Build a SaaS Service on Azure (Webinar Slide Deck)
|
||||
- Multitenant SaaS Micrososervice Architecture Diagram
|
||||
For more information please review the [ASDK Documentation](https://azure.github.io/azure-saas/), including a Quick Start guide for environment setup.
|
||||
|
||||
## Contributing
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit
|
||||
|
@ -57,5 +12,6 @@ When you submit a pull request, a CLA-bot will automatically determine whether y
|
|||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
|
||||
## License
|
||||
The Azure SaaS Development Kit is licensed under the MIT license. See the LICENSE file for more details.
|
|
@ -1,4 +1,4 @@
|
|||
baseURL = 'http://example.org/'
|
||||
baseURL = 'https://azure.github.io/azure-saas'
|
||||
title = 'Azure Saas Dev Kit(ASDK)'
|
||||
theme = "docsy"
|
||||
|
||||
|
@ -32,10 +32,13 @@ languageCode = "en-us"
|
|||
[params]
|
||||
|
||||
# Algolia
|
||||
algolia_docsearch = true
|
||||
algolia_docsearch = false
|
||||
offlineSearch = false
|
||||
|
||||
# GitHub Information
|
||||
github_repo = "https://github.com/Azure/azure-saas"
|
||||
github_subdir = "docs/azure-saas-docs"
|
||||
github_branch = "main"
|
||||
github_branch = "main"
|
||||
|
||||
[params.drawio]
|
||||
enable = false
|
|
@ -1,40 +1,62 @@
|
|||
---
|
||||
type: docs
|
||||
no_list: false
|
||||
linkTitle: "Welcome"
|
||||
linkTitle: "Introduction"
|
||||
weight: 0
|
||||
---
|
||||
# Welcome to the Azure SaaS Dev Kit
|
||||
# Welcome to the Azure SaaS Dev Kit!
|
||||
|
||||
[Software as a Service (SaaS)](https://azure.microsoft.com/en-us/overview/what-is-saas/) doesn’t need to be complex and time consuming.
|
||||
|
||||
The Azure SaaS Development Kit is a set of pre-built modular resources to help you launch your SaaS offering faster:
|
||||
The Azure SaaS Development Kit is a deployable reference implementation of pre-built modules designed to help you launch your SaaS offering faster:
|
||||
|
||||
* Standard SaaS components deployable individually or mix-and-match
|
||||
* [Open-source code](https://github.com/Azure/azure-saas), allowing engineers to build by example or modify/extend to be purpose-built
|
||||
* Fully documented code allows for self-serve usage
|
||||
* It includes commonly needed SaaS components that implement features such as identity, onboarding, and tenant management.
|
||||
* It is [100% open-source code](https://github.com/Azure/azure-saas), allowing you to build by example or modify/extend to be purpose-built for your particular scenario.
|
||||
* The code is fully documented, making it clear how the code functions, and why key design decisions were made.
|
||||
|
||||
Whether you're modernizing an existing application, building a new application, or migrating your application, the SaaS dev kit can help you.
|
||||
## Usage Options
|
||||
|
||||
## Modules & Architecture
|
||||
How you use the dev kit is up to you, here are some ideas to get you started:
|
||||
|
||||
![](futurestate.drawio.png)
|
||||
* Modernizing an existing application to support [full multitenancy](https://docs.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models#fully-multitenant-deployments) as part of a shift to a SaaS based business model.
|
||||
* Developing a greenfield SaaS offering for the first time.
|
||||
* Migrating a SaaS offering from another cloud to Azure.
|
||||
|
||||
- [**B2C Authentication Service**](/services/b2c-auth-service/) - Provides a flexible identity solution.
|
||||
- [**Core App**](/services/core-app/)
|
||||
- A web app where your customers to view plans and onboard to your solution.
|
||||
- Provides you with tenant administration capabilities. (modify/remove/etc.)
|
||||
- [**Saas.Application**](/services/saas-application/) - A sample application that you can extend or replace with your own code.
|
||||
|
||||
## Product SaaS Reference
|
||||
This is not a one-size-fits-all solution. Use as little or as much as you like. It is designed to be both a modular deployable reference implementation and also a reference architecture. You are free to use and change the code contained within this project in any way you'd like (following the terms of the [license](https://github.com/Azure/azure-saas/blob/main/LICENSE))):
|
||||
|
||||
[OnSubscriber](https://www.onsubscriber.com/) is a live production SaaS solution built entirely on the Azure SaaS Development Kit.
|
||||
* Deploy the entire solution to Azure using the [Bicep](https://docs.microsoft.com/azure/azure-resource-manager/bicep/) templates we provide, make changes to fit your exact use case, and start building your SaaS application from there.
|
||||
* Deploy one or more of our modules and hook it into your existing SaaS application.
|
||||
* Reference our code & architecture and build something entirely custom using the best practices we outlined.
|
||||
* Or, simply gain inspiration from this codebase.
|
||||
|
||||
It's a real production site that allows users to easily build their Email and SMS lists using social login with Facebook, Google, Apple, Email and SMS. In addition, users can add profile pictures, bios, external links and social accounts to their tenants.
|
||||
## Modular Architecture
|
||||
|
||||
This kit uses a [microservices architecture](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/microservices-architecture) so that each module is self-contained and can be used independently.
|
||||
|
||||
* [**Identity Framework**](components/identity)
|
||||
* [**Identity Provider (Azure AD B2C)**](components/identity/identity-provider/) - An identity solution that provides flexibility for both local accounts and integration with external providers.
|
||||
* [**Permissions Service**](components/identity/permissions-service) - An API that manages all user permissions and serves to enrich the user tokens returned from the identity provider.
|
||||
* [**Signup / Administration**](components/signup-administration/)
|
||||
* A web app where your customers view plans and onboard to your solution.
|
||||
* Provides you with tenant administration capabilities such as modify, remove, etc.
|
||||
* [**Admin Service**](components/admin-service) - An extensible API to programatically manage CRUD operations on tenants.
|
||||
|
||||
* [**SaaS.Application**](components/saas-application/) - A sample end user application that you can extend or replace with your own code.
|
||||
|
||||
> The Azure SaaS Dev Kit uses a [**fully multitenant deployment**](https://docs.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models#fully-multitenant-deployments). Multitenancy is a complex topic with many facets, and there is no *one size fits all* approach.
|
||||
>
|
||||
> Read more about multitenant architectures considerations and approaches at [https://aka.ms/multitenancy](https://aka.ms/multitenancy).
|
||||
|
||||
![](/azure-saas/diagrams/overview.drawio.png)
|
||||
|
||||
## Ready to get started?
|
||||
|
||||
Check out the [quick start page](quick-start/).
|
||||
|
||||
## Additional Recommended Resources
|
||||
|
||||
* [Best practices for architecting multi-tenant solutions](https://aka.ms/multitenancy)
|
||||
* [ISV Considerations for Azure landing zones](https://aka.ms/isv-landing-zones)
|
||||
* [Azure Well-Architected Framework](https://docs.microsoft.com/en-us/azure/architecture/framework/)
|
||||
* [WingTips Tickets SaaS Application](https://docs.microsoft.com/en-us/azure/azure-sql/database/saas-tenancy-welcome-wingtip-tickets-app) - Provides details into tradeoffs with various tenancy models within the database layer.
|
||||
* [WingTips Tickets SaaS Application](https://docs.microsoft.com/en-us/azure/azure-sql/database/saas-tenancy-welcome-wingtip-tickets-app) - Provides details into tradeoffs with various tenancy models within the database layer.
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Identity Framework"
|
||||
linkTitle: "Identity Framework"
|
||||
weight: 30
|
||||
---
|
||||
|
||||
The goal of our identity and authorization strategy is to enable us to easily authenticate users and provide basic Role Based Access Control (RBAC) for entities created within the application.
|
||||
|
||||
> Recommended Reading:
|
||||
> * [What is Azure Active Directory B2C?](https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview)
|
||||
> * [Architect multitenant solutions on Azure](http://aka.ms/multitenancy)
|
||||
|
||||
## Overview
|
||||
|
||||
Our Identity Framework is comprised of two main pieces:
|
||||
|
||||
1. Identity Provider (IdP) - Performs user login/authentication and provides a JWT token to the web applications. ASDK comes with Azure AD B2C implemented as the IdP out of the box.
|
||||
|
||||
2. Permissions Service - A microservice that tracks what tenants and data each user has access to and serves as an endpoint for the IdP to enrich the user's token with permissions and role claims during the login flow.
|
||||
|
||||
The Identity Framework also has a dependency on the [Microsoft Graph API](https://docs.microsoft.com/en-us/graph/overview), which we use to look up user information when needed.
|
||||
|
||||
## Architecture Diagram
|
||||
![Identity Diagram](/azure-saas/diagrams/identity-diagram.drawio.png)
|
||||
## Sign Up
|
||||
|
||||
> See the [Sign Up](./identity-flows/#sign-up) flow under *Identity Flows*
|
||||
|
||||
Upon clicking the signup button in either the SignupAdministration site or the end user application, the user is redirected to an Azure AD B2C hosted signup page. After providing the necessary information and submitting the signup form, Azure AD B2C will create the account and redirect the user back to the originating application with a JWT token.
|
||||
|
||||
## Sign In
|
||||
|
||||
> See the [Sign In](./identity-flows/#sign-in) flow under *Identity Flows*
|
||||
|
||||
Upon clicking the Sign In button in either the SignupAdministration site or the end user application, the user is redirected to an Azure AD B2C hosted signup page. After successfully authenticating (either with a local or federated account), Azure AD B2C makes a call to a route on the [Permissions Service](permissions-service/) to retrieve role and permissions information. Upon receiving this data, Azure AD B2C injects data in the form of custom claims and redirects the user back to the originating application with a JWT token.
|
||||
|
||||
## More Identity Topics
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Caveats & Limitations"
|
||||
weight: 100
|
||||
---
|
||||
|
||||
There are a few caveats of the identity solution provided within this reference implementation that you should be aware of.
|
||||
|
||||
- This reference implementation does not provide support for per-tenant “local” users. All users will be stored in a single Azure AD B2C tenant and the user objects themselves will not be separated by the tenant they signed up with. For example, if you had `jill@contoso.com` sign up to tenant 1, they would also be able to sign into tenant 2, tenant 3, and tenant 4 with the same `jill@contoso.com` account. You may still control what tenants they have roles under via the permissions that come back in their JWT claims.
|
||||
|
||||
- The current version only supports “local” users and social identities and does not provide support for configuring federation with other Identity Providers.
|
||||
|
||||
- Future versions will likely not provide support for “per-tenant federation” (i.e., where each tenant could bring their own IdP). This is primarily due to limitations in Azure AD B2C which introduce significant overhead when attempting to manage “per-tenants” users & policies within a directory.
|
||||
- It is possible, but each federation is configured via directory-wide policy and there is a limit of 200 policies on a directory.
|
||||
- If all tenants can be assumed to have their own Azure Active Directory (regular B2B), then per-tenant federation could be implemented using Azure AD (multitenant) federation identity provider with the application code doing the authorization based on specific tenant id claim. However, if each tenant wants to be able to configure their own completely different IdP (e.g., Okta, Ping, Auth0, Cognito), it would require additional work due to policy limits.
|
||||
|
||||
- Azure AD B2C has a documented limitation preventing API chaining via the OAuth 2.0 On-Behalf-Of flow. You may request a token to call an API via a web app, but not an API via an API. See the [Azure AD B2C Limitations](https://github.com/AzureAD/microsoft-identity-web/wiki/b2c-limitations) page for more information.
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Identity Flows"
|
||||
weight: 60
|
||||
---
|
||||
|
||||
## Sign Up
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor user as User
|
||||
participant frontend as Frontend Application
|
||||
participant auth as Auth Service (B2C)
|
||||
|
||||
|
||||
user->>frontend : Register (/register)
|
||||
frontend-->>user : Redirect to B2C Hosted Sign Up Page
|
||||
user->>auth : Sign Up Submitted
|
||||
auth->>auth : Create Account
|
||||
auth-->>user : Redirect with JWT
|
||||
```
|
||||
|
||||
## Sign In
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor user as User
|
||||
participant frontend as Frontend Application
|
||||
participant auth as Auth Service (B2C)
|
||||
participant perm as Permissions API
|
||||
|
||||
|
||||
user->>frontend : Login (/login)
|
||||
frontend-->>user : Redirect to B2C Hosted Sign In Page
|
||||
user->>auth : Login Submitted
|
||||
auth->>perm : Get Permissions & Roles
|
||||
perm-->>auth : Permissions & Roles
|
||||
auth->>auth : Add Custom Claims to JWT
|
||||
auth-->>user : Redirect with JWT
|
||||
```
|
||||
|
||||
## Add Permissions Record (Generic)
|
||||
|
||||
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant frontend as Frontend Application
|
||||
participant admin as Admin API
|
||||
participant perm as Permissions API
|
||||
|
||||
frontend->>admin : Add Tenant Permission for User
|
||||
admin->>admin : Is Requestor Admin of Tenant?
|
||||
admin->>perm : Add Tenant Permission for User
|
||||
perm->>perm : Permission Added in DB
|
||||
perm-->>admin : Ok
|
||||
admin-->>frontend : Ok
|
||||
|
||||
```
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Identity Provider"
|
||||
weight: 50
|
||||
---
|
||||
## Overview
|
||||
|
||||
For the identity framework, we chose to use [Azure AD B2C](https://docs.microsoft.com/azure/active-directory-b2c/overview) as our default identity provider. If you'd like to use another identity provider such as Azure AD or a different 3rd party tool, you can swap it out.
|
||||
|
||||
### What does Azure AD B2C give us?
|
||||
|
||||
Azure AD B2C provides business-to-customer identity as a service. It enables you to easily authenticate users to your application using their preferred identity provider and is configurable to support a wide array of scenarios.
|
||||
|
||||
### Configuration
|
||||
|
||||
Azure AD B2C has two methods of configuring the business logic that users follow to gain access to your application: [User Flows and Custom Policies](https://docs.microsoft.com/azure/active-directory-b2c/user-flow-overview). User Flows are predefined and are configured directly through the Azure AD B2C Web Portal. Custom Policies are XML based configuration files that are uploaded to the Azure AD B2C tenant.
|
||||
|
||||
The ASDK project uses Custom Policies to configure the Azure AD B2C tenant. The XML configuration that gets deployed can be found under the [Saas.IdentityProvider](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.IdentityProvider) folder within the repo, and you can read more about how to configure custom policies [here](https://docs.microsoft.com/azure/active-directory-b2c/user-flow-overview).
|
||||
|
||||
When deploying the Azure AD B2C Identity Provider via the instructions found in the [Quick Start](../../../quick-start) guide, Azure AD B2C is configured to do the following:
|
||||
|
||||
- Provide a hosted SignIn and SignUp page that users can be directed to
|
||||
- Reach out to the [SaaS.Permissions.Service](../permissions-service) upon a user signing in to fetch their application permissions and roles
|
||||
|
||||
You can change or extend the behavior of the Azure AD B2C tenant that gets deployed with ASDK to do things like collect more information during signup, force users to enroll in Multi-Factor Authentication (MFA), and much more by modifying the custom policies.
|
||||
|
||||
### App Roles and Global Admin
|
||||
|
||||
We are using [App Roles](https://docs.microsoft.com/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) to grant users "Global Admin" capabilities for the application. This App Role should only be granted to staff users that need it to administrate ALL the tenants across the entire SaaS solution. These roles are stored directly in Azure AD B2C and are returned in the JWT token claims when the user signs in.
|
||||
|
||||
**How do I add a user to the default Global Admin role?**
|
||||
|
||||
If you followed our steps in the [Quick Start](../../../quick-start), the user that created the Azure AD B2C tenant will be automatically added to this global admin role. Follow these steps if you'd like to add additional users:
|
||||
|
||||
1. Switch to your Azure AD B2C Tenant in the Azure portal
|
||||
|
||||
2. Navigate to the Azure Active Directory [menu](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview)
|
||||
|
||||
3. Click "Enterprise Applications"
|
||||
|
||||
4. Under the "Application Type" dropdown, select "All Applications" and click "Apply"
|
||||
|
||||
5. Select the `asdk-admin-api` app ![enterprise apps](/azure-saas/images/aad-enterprise-apps.png)
|
||||
|
||||
6. Select "Users and Groups" from the menu on the left
|
||||
|
||||
7. Click "Add user/group" ![Add user/groups](/azure-saas/images/aad-enterprise-apps-users-groups.png)
|
||||
|
||||
8. Select the users you'd like to add to the app role
|
||||
|
||||
9. Click "Assign"
|
||||
|
||||
10. Repeat steps 5-9, but on the `asdk-b2c-web` app instead
|
||||
|
||||
## Design Considerations and FAQ
|
||||
|
||||
- Q: Why did we choose Azure AD B2C?
|
||||
- A: We chose Azure AD B2C because in additional to authenticating with "local" accounts, it can be easily extended to support a wide array of other identity providers such as Azure AD, GitHub, and many more. See the [documentation](https://docs.microsoft.com/azure/active-directory-b2c/add-identity-provider) for details.
|
||||
|
||||
- Q: Why did we choose custom policies over user flows?
|
||||
- A: User Flows are predefined and meant for more basic use cases. Custom Policies provide more support for automating the setup and deployment of the Azure AD B2C configuration, and generally provide greater extensibility in the long term for more complicated scenarios.
|
||||
|
||||
- Q: Why are we only using Azure AD B2C App Roles for global administrator permissions? Why did we choose to put the tenant permissions in a special API?
|
||||
- A: App roles in Azure AD B2C are nice, but too many of them get extremely complicated to manage. You can absolutely achieve application tenant permissions using just app roles, but we wouldn't reccomend it if you are going to have more than just a handful of tenants. That's why we chose to separate the application tenant permissions into a special API/data store that gets called during the user login flow.
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
type: docs
|
||||
title: "SaaS.Permissions.Service"
|
||||
weight: 55
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The [SaaS.Permissions.Service](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.Permissions) module (aka Permissions Service) is a component of the [Identity Framework](../). It is an API that serves 2 main functions:
|
||||
|
||||
1. Handles Create, Read, Update, and Delete (CRUD) operations from the rest of the solution for permission data
|
||||
2. Serves as an endpoint for the [Identity Provider](../identity-provider) to retrieve permission data in order to enrich the user token with claims
|
||||
|
||||
## How to Run Locally
|
||||
|
||||
Instructions to get this module running on your local dev machine are located in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.Permissions).
|
||||
|
||||
### Configuration and Secrets
|
||||
|
||||
A list of app settings and secrets can be found in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.Permissions). All non-secret values will have a default value in the `appsettings.json` file. All secret values will need to be set using the [.NET Secret Manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows) when running the module locally, as it is not recommended to have these secret values in your `appsettings.json` file.
|
||||
|
||||
When deployed to Azure, the application is configured to load its secrets from [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/overview) instead. If you deploy the project using our ARM/Bicep templates from the Quick Start guide, the modules will be deployed to an Azure App Service which accesses the Azure Key Vault using a [System Assigned Managed Identity](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview). The Permissions Service module is also configured with [key name prefixes](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#use-a-key-name-prefix) to only import secrets with the prefix of `permissions-`, as other modules share the same Azure Key Vault.
|
||||
|
||||
## Module Design
|
||||
|
||||
### Dependencies
|
||||
|
||||
- SQL Server Database
|
||||
- Microsoft Graph API
|
||||
|
||||
### Consumers
|
||||
|
||||
- [Identity Provider](../identity-provider) (Azure AD B2C)
|
||||
- [Admin Service](../../../components/admin-service)
|
||||
|
||||
### Authentication
|
||||
|
||||
The Permissions Service is secured using [TLS Mutual Authentication](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-6.0#configure-certificate-validation) (Certificate Authentication). The application layer is configured to verify the authenticity of the certificate by comparing the thumbprint of the certificate against a configuration value. For TLS Mutual Authentication to work properly, the web server must also be configured to forward the certificate to the application layer. This is done for you if you deploy the application following the steps in the [Quick Start](../../quick-start) guide, but if you choose to run the code in an environment you provision, you will need to do [this configuration](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth) yourself.
|
||||
|
||||
> **Important!**
|
||||
> The certificate that gets deployed out of the box by following the Quick Start is a self signed certificate. Self signed certificates are not meant for production use, and it highly reccomended that you replace this certificate with one signed by a Certificate Authority before using this module in production.
|
||||
|
||||
### Database
|
||||
|
||||
[Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) is used to manage the SQL Server Database schema and connections. We are using [Code First Development](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/workflows/new-database) and, if no data or schema exists in the database on application startup, the application will automatically create the database schema that is defined in our [model files](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service/Data). If you make any changes to these models, you will need to preform a [migration](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/) to upgrade the database schema.
|
||||
|
||||
### Microsoft Graph API
|
||||
|
||||
The [Microsoft Graph API](https://docs.microsoft.com/en-us/graph/overview) is an API that provides a unified experience for accessing data on users within an Azure AD or Azure AD B2C tenant. Since we are using Azure AD B2C as our default Identity Provider, we must also use the Graph API when it becomes necessary to fetch data on our users. If you'd like to replace the identity provider with something else, you must also replace the Graph API calls within the permissions service to gather user data. These areas are clearly labeled with comments inline with the code.
|
||||
|
||||
### Swagger
|
||||
|
||||
The Permissions Service uses [Swashbuckle](https://www.nuget.org/packages/Swashbuckle) to generate the OpenAPI definition and a UI for testing. This definition is also consumed by the Admin Service to generate its client implementation for interfacing with this API. Read more about using it [here](https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-6.0&tabs=visual-studio).
|
||||
|
||||
## FAQ and Design Considerations
|
||||
|
||||
- Q: Why did we choose to secure the permissions service with certificate authentication over API Keys/JWT Tokens/Another Method?
|
||||
- A: The communication between Azure AD B2C (our default Identity Provider) and the permissions service must be secured with either [Basic or Certificate Auth](https://docs.microsoft.com/en-us/azure/active-directory-b2c/add-api-connector-token-enrichment?pivots=b2c-custom-policy#configure-the-restful-api-technical-profile) and it is not considered best practice to use Basic authentication in a production environment.
|
||||
|
||||
- Permissions are stored in the database in a single table (dbo.Permissions) with 3 pieces of data: Tenant ID, User ID (Email), and PermissionString. All 3 together make the row unique (i.e., you cannot have the same Permission for the same user on the same tenant more than once). Permissions are stored as a string (ex: Admin, User.Read, User.Write) for simplicity and extensibility. You may choose to store these in a separate database table and reference them by ID number if you have a large number of permissions and you want to enforce the types of permissions being assigned.
|
||||
- We have purposefully chosen to flow all CRUD operations on permissions through the [Admin Service](../../../components/admin-service). This is for a number of reasons:
|
||||
1. It removes the burden of authorization from the permissions service. All the permissions service needs to worry about is accepting a valid certificate, which only the identity provider and admin service possess. For higher security applications, you may choose to preform more authorization checks before adding permissions
|
||||
2. It simplifies the architecture. The frontend applications do not need to have any knowledge of the permissions service existing. When a tenant is created, the applications make 1 call to the admin service, and it handles the subsequent call to update the permissions records.
|
|
@ -1,9 +1,7 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Azure SaaS Dev Kit Services"
|
||||
linkTitle: "Services"
|
||||
title: "Azure SaaS Dev Kit Components"
|
||||
linkTitle: "Components"
|
||||
weight: 30
|
||||
description: "Learn about the building blocks of the Azure SaaS Dev Kit"
|
||||
---
|
||||
|
||||
Welcome to the Azure SaaS Dev Kit!
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
type: docs
|
||||
title: "SaaS.Admin.Service"
|
||||
weight: 50
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The [SaaS.Admin.Service](https://github.com/Azure/azure-saas/tree/main/src/Saas.Admin) module (aka Admin Service) is an API that has two main responsibilities:
|
||||
|
||||
1. Preforming Create, Read, Update, and Delete (CRUD) operations on tenants
|
||||
2. Serving as a broker to the permissions API to assign roles and permissions to tenants
|
||||
|
||||
## How to Run Locally
|
||||
|
||||
Instructions to get this module running on your local dev machine are located in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.Admin).
|
||||
|
||||
### Configuration and Secrets
|
||||
|
||||
A list of app settings and secrets can be found in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.Permissions). All non-secret values will have a default value in the `appsettings.json` file. All secret values will need to be set using the [.NET Secret Manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows) when running the project locally, as it is not recommended to have these secret values in your `appsettings.json` file.
|
||||
|
||||
When deployed to Azure, the application is configured to load in its secrets from [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/overview) instead. If you deploy the project using our ARM/Bicep templates from the Quick Start guide, the modules will be deployed to an app service which accesses the Azure Key Vault using a [System Assigned Managed Identity](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview). The Admin Service module is also configured with [key name prefixes](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#use-a-key-name-prefix) to only import secrets with the prefix of `admin-`, as other modules share the same Azure Key Vault.
|
||||
|
||||
## Module Design
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [SaaS.Permissions.Service](../identity/permissions-service)
|
||||
- Depends on the SaaS.Permissions.Service for CRUD operations on permissions records
|
||||
- [Identity Provider](../identity/identity-provider)
|
||||
- Depends on the identity provider to authenticate callers via their JWT token
|
||||
|
||||
### Consumers
|
||||
|
||||
Currently, the only consumers of this API are the 2 frontend applications. Every module in the ASDK project was designed to be extensible, so you could also build your own applications that interface directly with this API to fit other use cases that the included frontend applications do not provide.
|
||||
|
||||
- [SaaS.SignupAdministration.Web](../signup-administration)
|
||||
|
||||
- [Saas.Application.Web](../saas-application)
|
||||
|
||||
### Authentication
|
||||
|
||||
The Admin Service is secured using OAuth 2.0 authentication via the Microsoft Identity Platform. Incoming requests must contain a valid JWT Bearer token in the `Authorization` header. The token must contain a valid scope that the calling application has been authorized to use. To learn more about how this process works and is configured in Azure AD B2C, we highly recommend checking out our list of [identity resources & documentation](../../resources/additional-recommended-resources/#identity-focused).
|
||||
|
||||
For authorization, we have also included middleware on the Admin Service that extracts the user's permission records from the JWT token claims and performs authorization based on policies applied at the route level. In other words, before preforming any action on a tenant once a request is received, the admin service will first ensure that the user making the request has a claim to that tenant on their token. If there is no claim to that tenant, or their role does not match what they're trying to do, the request will be denied.
|
||||
|
||||
Implementing this in a multitenant fashion is often the most difficult part of starting a SaaS project, so we tried to make it as extensible as possible to support a wide range of scenarios.
|
||||
|
||||
### Database
|
||||
|
||||
[Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) is used to manage the SQL Server Database schema and connections. We are using [Code First Development](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/workflows/new-database) and, if no data or schema exists in the database on application startup, the application will automatically create the database schema that is defined in our [model files](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service/Data). If you make any changes to these models, you will need to preform a [migration](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/) to upgrade the database schema.
|
||||
|
||||
### Swagger and NSwag
|
||||
|
||||
The Admin Service uses [Swashbuckle](https://www.nuget.org/packages/Swashbuckle) to generate the OpenAPI definition and a UI for testing. This definition is also consumed by the Signup Administration site to generate its client implementation for interfacing with this API. Read more about using it [here](https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-6.0&tabs=visual-studio).
|
||||
|
||||
In addition, it also uses NSwag to consume the OpenAPI definition for the Permissions Service to generate a client implementation for that. Read more about NSwag [here](https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-nswag?view=aspnetcore-6.0&tabs=visual-studio).
|
||||
|
||||
## FAQ and Design Considerations
|
||||
|
||||
- Q: Why did we choose to use strings to store our IDs and not GUIDs?
|
||||
- A: The default implementation that we chose is to use GUIDs for IDs, but store them as strings. This decisision was made so that consumers of the ASDK project would not be forced into using GUIDs, should they want to use something else for IDs.
|
||||
|
||||
- Q: Why did we choose to not give the admin service its own Azure Key Vault?
|
||||
- A: For simplicity, we decided to use key name prefixes to store keys for each module across the entire application into one Azure Key Vault. This is an acceptable approach, but, if necessary, you may choose to separate the secrets for each module into a dedicated Azure Key Vault.
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
type: docs
|
||||
title: "SaaS.Application.Web"
|
||||
weight: 90
|
||||
---
|
||||
## Overview
|
||||
|
||||
The [SaaS.Application.Web](https://github.com/Azure/azure-saas/tree/main/src/Saas.Application) module is a stub application demonstrating where users of the ASDK project would insert their code. It is the end user, customer facing application written in ASP.NET 6.0 that the multitenant SaaS solution is designed around.
|
||||
|
||||
## How to Run Locally
|
||||
|
||||
Instructions to get this module running on your local dev machine are located here:
|
||||
https://github.com/Azure/azure-saas/tree/main/src/Saas.Application
|
||||
|
||||
## Design
|
||||
|
||||
### Dependencies
|
||||
|
||||
None
|
||||
|
||||
### Consumers
|
||||
|
||||
End Users
|
||||
|
||||
## How do I use this application?
|
||||
|
||||
This application is designed for you to immediately start writing your own code. All you need to do to use this module is clone the ASDK repo and begin building your SaaS application inside of it. If you are not using ASDK in a greenfield scenario (ie you're porting your application or otherwise), you may choose to replace this application entirely with one that you have already.
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
type: docs
|
||||
title: "SaaS.Notifications"
|
||||
weight: 100
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The SaaS.Notifications module is a relatively simple [Azure Logic App](https://docs.microsoft.com/en-us/azure/logic-apps/logic-apps-overview) that gets deployed to enable email sending from the rest of the solution. It is deployed with an HTTP trigger that takes in a JSON payload with fields required to send an email.
|
||||
|
||||
## Design
|
||||
### Dependencies
|
||||
|
||||
- Email provider of choice, once configured
|
||||
### Consumers
|
||||
|
||||
- [SaaS.SignupAdministration.Web](../signup-administration)
|
||||
|
||||
|
||||
### Authentication
|
||||
|
||||
The logic app is using the default [SAS Token](https://docs.microsoft.com/en-us/azure/logic-apps/logic-apps-securing-a-logic-app?tabs=azure-portal#generate-shared-access-signatures-sas) that gets generated with the HTTP trigger. Take care to not commit this SAS token into your repo, as it is considered a secret. Anyone with access to this URL with the SAS token will have permission to call your logic app. If you deploy the Azure SaaS Dev Kit using the Quick Start guide, this will be automatically uploaded to a keyvault for reference via the applications.
|
||||
## Logic App Configuration
|
||||
|
||||
### Input
|
||||
The logic app comes pre-configured with a default JSON schema with some common email data fields. You may edit this to your liking by going to the logic app designer for the logic app that is deployed in your environment. Here is the default JSON schema that gets deployed:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"HTML": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailFrom": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailToName": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
### Email Provider
|
||||
By default, the logic app that gets deployed does not come with an email provider configured. If you POST to the endpoint, the logic app will take in the data and simply return a 200 OK response. To enable actual email sending, you must first configure an email provider [connector](https://docs.microsoft.com/en-us/connectors/connector-reference/connector-reference-logicapps-connectors).
|
||||
|
||||
To do this, follow these instructions:
|
||||
1. Navigate to the deployed logic app and click into the "Logic app designer" menu
|
||||
2. Click the + button underneath the defined HTTP trigger and click "Add an action"
|
||||
![](/azure-saas/images/logic-app-1.png)
|
||||
3. Search for your email connector of choice and choose the action for sending an email (ex, for Office 365 it is "Send an Email (V2)")
|
||||
4. Fill in the required fields with data from the "Dynamic content" section. This will pass data from the input HTTP trigger to the email connector
|
||||
![](/azure-saas/images/logic-app-2.png)
|
||||
5. Click "Save"
|
||||
6. You may now test using the application, or by clicking "Run trigger" at the top of the logic app designer
|
||||
|
||||
|
||||
## Additional Reading
|
||||
|
||||
- [Connect to Office 365 Outlook using Azure Logic Apps](https://docs.microsoft.com/en-us/azure/connectors/connectors-create-api-office365-outlook)
|
||||
- [Connect to SendGrid from Azure Logic Apps](https://docs.microsoft.com/en-us/azure/connectors/connectors-create-api-office365-outlook)
|
||||
- [Tutorial: Send email and invoke other business processes from App Service](https://docs.microsoft.com/en-us/azure/app-service/tutorial-send-email?tabs=dotnet)
|
|
@ -0,0 +1,168 @@
|
|||
---
|
||||
type: docs
|
||||
title: "SaaS.SignupAdministration.Web"
|
||||
weight: 80
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The [SaaS.SignupAdministration.Web](https://github.com/Azure/azure-saas/tree/main/src/Saas.SignupAdministration) (aka SignupAdmin) module is a web application meant to faciliate self service onboarding to your SaaS product. End Users/Customers can visit this site to:
|
||||
|
||||
- Sign up for an account
|
||||
|
||||
- Go through an onboarding flow to create a new tenant
|
||||
|
||||
- Manage their existing tenants.
|
||||
|
||||
This site also supports administrative functionality for global administrators to view and manage all tenants and users of the application.
|
||||
|
||||
## How to Run Locally
|
||||
|
||||
Instructions to get this module running on your local dev machine are located in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.SignupAdministration).
|
||||
|
||||
### Configuration and Secrets
|
||||
|
||||
A list of app settings and secrets can be found in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.Identity/Saas.Permissions). All non-secret values will have a default value in the `appsettings.json` file. All secret values will need to be set using the [.NET Secrets Manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows) when running the module locally, as it is not recommended to have these secret values in your `appsettings.json` file.
|
||||
|
||||
When deployed to Azure, the application is configured to load in its secrets from [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/overview) instead. If you deploy the project using our ARM/Bicep templates from the Quick Start guide, the modules will be deployed to an app service which accesses the Azure Key Vault using a [System Assigned Managed Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview). The SignupAdmin module is also configured with [key name prefixes](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#use-a-key-name-prefix) to only import secrets with the prefix of `signupadmin-`, as other modules share the same Azure Key Vault.
|
||||
|
||||
## Module Design
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [SaaS.Admin.Service](../admin-service)
|
||||
- Depends on the SaaS.Admin.Service for CRUD operations on tenant records, as well as to broker the connection to the SaaS.Permissions.Service for CRUD operations on permissions records.
|
||||
- [Identity Provider](../identity/identity-provider)
|
||||
- Depends on the identity provider to authenticate users and receive a JWT token.
|
||||
- [SaaS.Notifications](../saas-notifications)
|
||||
- Depends on the SaaS.Notifications logic app to send transactional emails to users
|
||||
|
||||
### Consumers
|
||||
|
||||
- End Users/Customers
|
||||
|
||||
### Authentication
|
||||
|
||||
The SignupAdmin site uses the [Microsoft Authentication Library (MSAL)](https://docs.microsoft.com/azure/active-directory/develop/msal-overview) to handle the [flow](../identity/identity-flows#sign-in) of signing in and parsing the user token from the Azure AD B2C. The identity provider must be configured properly for this to work, and you must provide certain configuration values to the SignupAdmin site for it to properly communicate with Azure AD B2C. These config values can be found in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.SignupAdministration).
|
||||
|
||||
For communication with the SaaS.Admin.Service, the SignupAdmin site also uses the MSAL to request an access token, using the signed-in user's existing token, to use in the Authorization header of all requests. This new token is specific to the SaaS.Admin.Service and will be requested from the Identity Provider with a specific scope. This type of exchange is known as an [OAuth 2.0 On-Behalf-Of Flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow).
|
||||
|
||||
### NSwag
|
||||
|
||||
The NSwag project provides tools to generate OpenAPI specifications from existing ASP.NET Web API controllers and client code from these OpenAPI specifications. This provides us a ready-to-use HTTP client without having to write much boilerplate. Read more about [using NSwag on ASP.NET projects](https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-nswag?view=aspnetcore-6.0&tabs=visual-studio).
|
||||
|
||||
<!-- TODO: Add nswag config to project. -->
|
||||
<!-- You can also find the nswag configuration file we used to generate the client in this folder. -->
|
||||
|
||||
### Transactional Emails
|
||||
|
||||
When a new tenant is created, the SignupAdmin site will make a REST call to the [SaaS.Notifications](../saas-notifications) module. This module is an [Azure Logic App](https://docs.microsoft.com/en-us/azure/logic-apps/logic-apps-overview) that gets deployed with a basic endpoint that accepts an email request. You must configure your email provider in this logic app post deployment before emails will actually send from the SignupAdmin site.
|
||||
|
||||
## FAQ and Design Considerations
|
||||
|
||||
- For the onboarding workflow, we are keeping track of the state in memory. This is fine for demo purposes and low traffic systems, but this is not a great pattern when introducing horizontally scaled backends. It is recommended that you replace the JsonPersistenceProvider (the service that stores the state) with another implementation backed by an external cache (ex: Redis, CosmosDB, etc). Instructions on where to do this can be found in the module's [readme.md](https://github.com/Azure/azure-saas/tree/main/src/Saas.SignupAdministration)
|
||||
|
||||
- Q: How do I manage tenants created by this application?
|
||||
- A: For ease of management, we have chosen to incorporate the global administrative functionality into this application. You may choose to separate this functionality into a different module if you require more administrative functionality than just tenant and user management
|
||||
|
||||
- Q: What other implementation options exist for email notifications?
|
||||
- A: A better solution for sending transactional emails might be to emit an event or message from the application, and building a notifications system to subscribe to those events. You could then build other applications to consume those events for other business functions as well. This style of architecture is called an [Event Driven](https://docs.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven) architecture. We decided to not go this route (yet!) with this project to keep it simple, but it is something to be considered when looking at your overall technical landscape.
|
||||
|
||||
- Q: Why did we choose to not give the signup admin api its own Azure Key Vault?
|
||||
- A: For simplicity, we decided to use key name prefixes to store keys for each module across the entire application into one Azure Key Vault. This is an acceptable approach, but, if necessary, you may choose to separate the secrets for each module into a dedicated Azure Key Vault.
|
||||
|
||||
## Signup Administration User Flows
|
||||
|
||||
### Sign In
|
||||
|
||||
See the [Sign-In Flow](../identity/identity-flows#sign-in) in Identity Flows.
|
||||
|
||||
### Onboarding Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor user as Tenant Admin
|
||||
participant signup as Signup App
|
||||
participant admin as Admin Api
|
||||
participant auth as Auth Service
|
||||
participant perm as Permissions Api
|
||||
participant email as Email Logic App
|
||||
|
||||
user->>signup: Sign Up Button Clicked
|
||||
|
||||
signup->>signup: User Signed In/Token Exists?
|
||||
signup-->>user: No, Redirect to /login
|
||||
user->>auth : Sign in or Sign Up
|
||||
auth-->>user : JWT
|
||||
user->>signup: Sign Up Button Clicked
|
||||
signup->>signup: Token Exists?
|
||||
signup->>user : Yes, Start Onboarding Flow -- Org Name Page
|
||||
user->>signup: Org Name Provided
|
||||
signup-->>user : Category Select
|
||||
user->>signup: Select Category
|
||||
signup-->>user : Route Name Page
|
||||
signup->>user: Enter Route Name
|
||||
signup->>admin: Check if Route Exists
|
||||
admin->>signup: Route does not exist
|
||||
signup-->>user : Validation Page
|
||||
user->>signup : Submit
|
||||
signup->>admin : Create Tenant
|
||||
admin->>admin : Create Tenant
|
||||
admin->>perm : Add Admin Permission for Tenant
|
||||
perm-->>admin : Permission Added
|
||||
admin->>email : Send Tenant Created Confirmation Email
|
||||
email-->>admin : Sent
|
||||
admin-->>signup : Tenant Created
|
||||
admin-->>user : Tenant Created Confirmation Page
|
||||
```
|
||||
|
||||
### Add New Tenant Admin - Existing User
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor user as Tenant Admin
|
||||
participant signup as SignupAdministration Site
|
||||
participant admin as Admin API
|
||||
participant perm as Permissions API
|
||||
participant auth as Auth Service (B2C)
|
||||
|
||||
user->>signup : Get list of tenants
|
||||
signup->>admin : Get list of tenants for user
|
||||
admin-->>signup : List of tenants for user
|
||||
signup-->>user : List of tenants
|
||||
user->>signup : Add user to tenant by email
|
||||
signup->>admin : POST: Add user to tenant by email
|
||||
admin->>admin : Claim({tenantId}.users.write)
|
||||
admin->>perm : Add user to tenant by email
|
||||
perm->>auth : User exists?
|
||||
auth-->>perm : User exists
|
||||
perm->>perm : Add Permissions Record
|
||||
perm-->>admin : Ok
|
||||
admin-->>signup : Ok
|
||||
signup-->>user : Ok
|
||||
```
|
||||
|
||||
### Add New Tenant Admin - User Does Not Exist
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor user as Tenant Admin
|
||||
participant signup as SignupAdministration Site
|
||||
participant admin as Admin API
|
||||
participant perm as Permissions API
|
||||
participant auth as Auth Service (B2C)
|
||||
|
||||
user->>signup : Get list of tenants
|
||||
signup->>admin : Get list of tenants for user
|
||||
admin-->>signup : List of tenants for user
|
||||
signup-->>user : List of tenants
|
||||
user->>signup : Add user to tenant by email
|
||||
signup->>admin : POST: Add user to tenant by email
|
||||
admin->>admin : Claim({tenantId}.users.write)
|
||||
admin->>perm : Add user to tenant by email
|
||||
perm->>auth : User exists?
|
||||
auth-->>perm : User does not exist
|
||||
perm-->>admin : Error, User does not exist
|
||||
admin-->>signup : Error, User does not exist
|
||||
signup-->>user : Error, User does not exist
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Architecture Diagrams"
|
||||
linkTitle: "Architecture Diagrams"
|
||||
weight: 15
|
||||
---
|
||||
|
||||
### Overview & Dependencies
|
||||
|
||||
![](/azure-saas/diagrams/overview.drawio.png)
|
||||
|
||||
### Identity Framework
|
||||
|
||||
![](/azure-saas/diagrams/identity-diagram.drawio.png)
|
||||
|
||||
### User Types
|
||||
![](/azure-saas/diagrams/user-types.drawio.png)
|
|
@ -4,6 +4,10 @@ title: "FAQ"
|
|||
linkTitle: "FAQ"
|
||||
weight: 100
|
||||
description: "Frequently Asked Questions"
|
||||
toc_hide: true
|
||||
---
|
||||
|
||||
FAQ
|
||||
FAQ
|
||||
|
||||
|
||||
<!-- TODO: Fill out FAQ -->
|
Двоичные данные
docs/azure-saas-docs/content/en/futurestate.drawio.png
До Ширина: | Высота: | Размер: 155 KiB |
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Getting Started"
|
||||
linkTitle: "Getting Started"
|
||||
weight: 10
|
||||
description: "Getting Started with the Azure SaaS Dev Kit"
|
||||
---
|
||||
|
||||
Welcome to the Azure SaaS Dev Kit getting started guide!
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Azure SaaS Dev Kit Overview"
|
||||
linkTitle: "Overview"
|
||||
weight: 20
|
||||
description: "Learn about Dapr including its main features and capabilities"
|
||||
---
|
||||
|
||||
Welcome to the Azure SaaS Dev Kit!
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Contoso BadgeMeUp"
|
||||
weight: 20
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
![BadgeMeUp Screenshot](../badgemeup-screenshot.gif)
|
||||
|
||||
Contoso BadgeMeUp is a SaaS B2B application that Contoso sells to companies that want a great tool to improve the culture within their organization.
|
||||
|
||||
> For more information about how this SaaS architecture compares to others, please see *Scenario 1* in [SaaS Branding Considerations](../branding-considerations-for-saas/#scenario-1---pure-b2b).
|
||||
|
||||
Lucerne Publishing has recently purchased Contoso BadgeMeUp. They're currently using Azure Active Directory and want their employees to be able to Single Sign-on to BadgeMeUp.
|
||||
|
||||
> Note: Contoso IT has configured their B2C instance for an external authentication provider. Additional documentation is available [here]([Set up sign-in for multi-tenant Azure AD by custom policies - Azure AD B2C | Microsoft Docs](https://docs.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-azure-ad-multi-tenant?pivots=b2c-user-flow)).
|
||||
|
||||
### Onboarding
|
||||
|
||||
1. Phil in accounting browsed the plans available at BadgeMeUp.Contoso.com and selected the plan he thought would best fit their companies needs.
|
||||
2. Sandy navigates to BadgeMeUp.Contoso.com and is automatically signed on with her companies credentials.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
user["fa:fa-user Sandy - User"]
|
||||
isv["fa:fa-id-card BadgeMeUp"]
|
||||
accounting["Phil - Accounting"]
|
||||
|
||||
accounting-- "$" -->isv
|
||||
user-->isv
|
||||
subgraph Lucerne Publishing
|
||||
user
|
||||
accounting
|
||||
end
|
||||
subgraph "fa:fa-building Contoso"
|
||||
isv
|
||||
end
|
||||
```
|
||||
|
||||
> Note: Currently, Sandy has to sign up for the service. **How do we simplify?**
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Quick Start"
|
||||
linkTitle: "Quick Start"
|
||||
weight: 10
|
||||
description: "Getting Started with the Azure SaaS Dev Kit"
|
||||
---
|
||||
|
||||
On this page, you will find instructions for how to run the dev kit in your local environment, how to deploy the solution to Azure, and where to put your application code to customize the solution.
|
||||
|
||||
> Tip: If you're new here and want to learn what is Azure SaaS Dev Kit, check out the [welcome page](..)
|
||||
|
||||
## 1. Setup Identity Framework
|
||||
|
||||
This project uses [Azure Active Directory B2C](https://docs.microsoft.com/azure/active-directory-b2c/overview) for an IdP (Identity Provider). The first step in setting up this project is to configure a new Azure AD B2C instance to house your local user accounts. You will also need to deploy the [Permissions API](../components/identity/permissions-service), as Azure AD B2C will have a dependency on it.
|
||||
|
||||
To setup the Identity Framework, we have provided a PowerShell script [here]() that automates the setup for you. This PowerShell script will output a parameters file that you'll need to provide when deploying the solution to Azure in step 2.b.
|
||||
|
||||
After finishing the IDP setup, you may choose to either run the project locally first or immediately deploy the solution to Azure.
|
||||
## 2.a. Running the Dev Kit in your local dev environment
|
||||
|
||||
- Install the latest version of [Visual Studio 2022](https://visualstudio.microsoft.com/vs/). You may also use Visual Studio Code, but the solution and projects are targetted at VS2022.
|
||||
- Clone the repository `https://github.com/Azure/azure-saas.git` on to your dev machine.
|
||||
- Open the `.sln` in the root of the repository. This solution includes all of the modules.
|
||||
- Depending on the project you wish to run, you'll need to set some secrets to properly setup authentication with Azure AD B2C. See the [App Settings](#app-settings) section below.
|
||||
|
||||
### App Settings
|
||||
|
||||
- Running locally you will need to set some App Settings & User Secrets manually using the [.NET Secret Manager](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#secret-storage-in-the-development-environment).
|
||||
- Deployed to Azure, these secrets are automatically configured for you and stored in the Azure Key Vault.
|
||||
|
||||
Make sure you check out the [readme files](#more-info) in each project's directory for a description of the app settings & secrets you'll need to set in order to run the respective project.
|
||||
|
||||
|
||||
## 2.b. Deploying to Azure - Entire Solution
|
||||
|
||||
Deploying to Azure is easy thanks to our pre-configured ARM (Azure Resource Manager) templates.
|
||||
|
||||
This button will take you to the Azure portal and passing it the template. You'll be asked a few questions, and then the solution will be up and running in just a few minutes. You will need your Azure AD B2C configuration values and secrets from step 1.
|
||||
|
||||
**Deployment Coming Soon!**
|
||||
<!-- [![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.Deployment%2FSaas.Deployment.Root%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fmain%2Fsrc%2FSaas.Deployment%2FSaas.Deployment.Root%2FcreateUiDefinition.json) -->
|
||||
|
||||
|
||||
### How does this work?
|
||||
|
||||
This solution uses a Bicep template which is checked into source control. Whenever changes are detected, a GitHub pipeline compiles the template into an ARM template.
|
||||
|
||||
> What is Bicep?
|
||||
> Bicep is a domain-specific language (DSL) that uses declarative syntax to deploy Azure resources. In a Bicep file, you define the infrastructure you want to deploy to Azure, and then use that file throughout the development lifecycle to repeatedly deploy your infrastructure. Your resources are deployed in a consistent manner.
|
||||
|
||||
## 2.c. (Advanced) Deploying to Azure - Single Module
|
||||
|
||||
If you'd like to use just one (or more) module from the project, we've provided [Bicep](https://docs.microsoft.com/azure/azure-resource-manager/bicep/) templates to do that as well. In each project directory, you'll find a folder named `{ModuleName}.Deployment` that contains all the Bicep code you'll need to deploy just that Module. Please be advised that there are certain dependencies that each module requires in order for it to deploy properly. You may find that you need to edit the Bicep templates to match your use case. You will find instructions and a list of dependencies for each module within the [module's readme](#more-info).
|
||||
|
||||
## 3. (Optional) Configure Email Provider
|
||||
|
||||
The SaaS.Notifications module **need page and link** is an Azure Logic App responsible for generating email notifications. By default, there is no email provider configured. If you'd like to enable email notifications, you will need to manually configure your email provider connector of choice inside the Logic App. See the instructions [here](../components/saas-notifications) to get started.
|
||||
|
||||
## 4. Integrating your application
|
||||
|
||||
Now that you've seen how to run the code locally as well as deploy your code to Azure (in a repeatable and code-first way), you can integrate your own code into the solution.
|
||||
|
||||
We've included a basic application within the `Saas.Application.Web` project that demonstrates a SaaS solution called "BadgeMeUp". BadgeMeUp is simply a badge sharing site that *Contoso* (representing your company) can sell to end customers.
|
||||
|
||||
> SaaS solutions come in many shapes as sizes. We picked "BadgeMeUp", because it's a fairly simple scenario to understand. [You can read more about this particular SaaS scenario here](../resources/contoso-badgemeup/).
|
||||
|
||||
## More Info
|
||||
|
||||
For more information, including deployment instructions, an outline of dependencies, app settings, and more, check out the readme files for each module:
|
||||
|
||||
- [SaaS.Admin.Service Readme](https://github.com/Azure/azure-saas/tree/main/src/Saas.Admin)
|
||||
- [SaaS.Permissions.Service Readme](https://github.com/Azure/azure-saas/tree/main/src/Saas.Permissions)
|
||||
- [SaaS.SignupAdministration.Web Readme](https://github.com/Azure/azure-saas/tree/main/src/Saas.SignupAdministration)
|
||||
- [SaaS.Application Readme](https://github.com/Azure/azure-saas/tree/main/src/Saas.Application)
|
||||
|
||||
## Learn more about SaaS
|
||||
|
||||
There are a plethora of resources to help you on your SaaS journey. They're available in the [SaaS Resources section](../resources/additional-recommended-resources/).
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Resources"
|
||||
linkTitle: "Resources"
|
||||
weight: 40
|
||||
---
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Azure Recommended Resources"
|
||||
linkTitle: "Additional Recommended Resources"
|
||||
weight: 200
|
||||
---
|
||||
|
||||
* [Best practices for architecting multitenant solutions on Azure](https://aka.ms/multitenancy)
|
||||
* [ISV Considerations for Azure landing zones](https://aka.ms/isv-landing-zones)
|
||||
* [Azure Well-Architected Framework](https://docs.microsoft.com/en-us/azure/architecture/framework/)
|
||||
* [WingTips Tickets SaaS Application](https://docs.microsoft.com/en-us/azure/azure-sql/database/saas-tenancy-welcome-wingtip-tickets-app) - Provides details into tradeoffs with various tenancy models within the database layer.
|
||||
|
||||
## Identity Focused
|
||||
|
||||
* [OAuth 2.0 On-Behalf-Of flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow)
|
||||
* Please also see the [caveats](../../components/identity/caveats) page on this subject
|
||||
* [How to secure a .NET Web API using Azure AD B2C](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/4-WebApp-your-API/4-2-B2C)
|
||||
* [Permissions, Scope, and Consent in the Microsoft Identity Platform](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent)
|
До Ширина: | Высота: | Размер: 15 KiB После Ширина: | Высота: | Размер: 15 KiB |
|
@ -4,13 +4,13 @@ title: "SaaS Branding Considerations"
|
|||
weight: 10
|
||||
---
|
||||
|
||||
As a SaaS vendor, it's useful to understand where your branding ends, and the vendor branding begins. In other words, how will your software solution be positioned for your customer base.
|
||||
As a SaaS vendor, it's useful to understand where your branding ends, and the tenant branding begins. In other words, how will your software solution be positioned for your customer base?
|
||||
|
||||
> **Identity** plays a critical role within SaaS solutions. It's an area where extra planning early in your design is recommended.
|
||||
|
||||
## Common Customer Scenarios
|
||||
|
||||
These are common scenarios from the perspective of the customer / user. Understanding which scenario best aligns
|
||||
These are common scenarios from the perspective of the customer / user. Understanding which scenario best aligns to you is critical for designing your SaaS application for multitenancy.
|
||||
|
||||
### Scenario 1 - Pure B2B
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Contoso BadgeMeUp"
|
||||
weight: 20
|
||||
---
|
||||
|
||||
## Intro
|
||||
|
||||
For ease of explaining the project and who it might benefit, we created an example "story" to help users visualize the components. In the story below, Contoso is ISV who developed the BadgeMeUp SaaS platform and Lucerne Publishing is the customer who purchased BadgeMeUp for use by employees within their organization.
|
||||
|
||||
## Context
|
||||
|
||||
![BadgeMeUp Screenshot](../badgemeup-screenshot.gif)
|
||||
|
||||
Contoso is a SaaS ISV (software vendor) that has a product, Contoso BadgeMeUp. Contoso BadgeMeUp is a SaaS B2B application that Contoso sells to companies that want a great tool to improve the culture within their organization.
|
||||
|
||||
> For more information about how this SaaS architecture compares to others, please see *Scenario 1* in [SaaS Branding Considerations](../branding-considerations-for-saas/#scenario-1---pure-b2b).
|
||||
|
||||
Lucerne Publishing has recently purchased Contoso BadgeMeUp. They're currently using Azure Active Directory and want their employees to be able to Single Sign-on to BadgeMeUp.
|
||||
|
||||
> Note: Contoso IT has configured their Azure AD B2C instance for an external authentication provider. Additional documentation is available [here](https://docs.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-azure-ad-multi-tenant?pivots=b2c-user-flow).
|
||||
|
||||
### Onboarding
|
||||
|
||||
1. Phil in Lucerne Publishing accounting browsed the plans available at Signup.BadgeMeUp.Contoso.com and selected the plan he thought would best fit the company's needs.
|
||||
2. Sandy navigates to BadgeMeUp.Contoso.com/lucernepublishing and signs in using her Azure AD credentials.
|
||||
3. After Sandy creates an account, Phil can then go to Signup.BadgeMeUp.Contoso.com/admin to see his previously created tenant and grant Sandy elevation permissions if he wishes.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
user["fa:fa-user Sandy - User"]
|
||||
isv["fa:fa-id-card BadgeMeUp"]
|
||||
accounting["Phil - Accounting"]
|
||||
|
||||
accounting-- "$" -->isv
|
||||
user-->isv
|
||||
subgraph Lucerne Publishing
|
||||
user
|
||||
accounting
|
||||
end
|
||||
subgraph "fa:fa-building Contoso"
|
||||
isv
|
||||
end
|
||||
```
|
До Ширина: | Высота: | Размер: 275 KiB После Ширина: | Высота: | Размер: 275 KiB |
|
@ -2,6 +2,7 @@
|
|||
type: docs
|
||||
title: "Contoso Tickets"
|
||||
weight: 20
|
||||
toc_hide: true
|
||||
---
|
||||
|
||||
## Context
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
type: docs
|
||||
title: "DevOps Workflows"
|
||||
weight: 100
|
||||
toc_hide: true
|
||||
---
|
||||
|
||||
For your convenience, we have provided some sample GitHub workflows that you can use to build and deploy code changes to each module. These workflows are the same ones that we use to package and release the code to our test environment. They were created as a baseline reference with the intent to be extensible when needed.
|
||||
|
||||
## How does it work?
|
||||
There are 2 [reusable workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) we have created which the process is based on.
|
||||
|
||||
Each module has 2 workflow files that implement these re-usable workflows ensuring each build and deploy step taken is consistent across the entire solution. Each module workflow file is responsible for defining when the workflow is triggered, setting up variables, and calling the correct reusable workflow.
|
||||
|
||||
### Process Outline
|
||||
### **Build & Deploy to Staging**
|
||||
Workflow File: `template-pr-deploy.yml`
|
||||
|
||||
- `pr-deploy-saas-admin.yml`
|
||||
- `pr-deploy-saas-application.yml`
|
||||
- `pr-deploy-saas-permissions.yml`
|
||||
- `pr-deploy-saas-signupadministration.yml`
|
||||
|
||||
Triggered On: `Pull Request targeting main branch`
|
||||
|
||||
#### Input Variables
|
||||
|
||||
| Input Name | Description | Default |
|
||||
|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------|
|
||||
| dotnet_version | The version of the .NET project to build | |
|
||||
| artifact_name | The name of the artifact file produced | |
|
||||
| app_service_name | The name of the app service to deploy the artifact to | |
|
||||
| app_service_resource_group_name | The name of the resource group the app service resides in | |
|
||||
| project_build_path | The path of the folder that the .csproj for the module is in | |
|
||||
| slot_name | The name of the deployment slot to create and deploy to on the app service (Used for override, recommended to keep the default) | pr-${{github.event.pull_request.number}} |
|
||||
|
||||
#### Secrets
|
||||
|
||||
- `AZURE_CREDENTIALS`
|
||||
- The CI/CD pipeline must contain Azure credentials for a service principal that has an appropriate access level to create a new Azure App Service slot and deploy to it. See [this](https://docs.microsoft.com/azure/developer/github/connect-from-azure?tabs=azure-portal%2Cwindows) document for setting up these credentials in your own repo.
|
||||
|
||||
#### Job Breakdown
|
||||
There are 3 jobs within this workflow: `build`, `create-deployment-slot`, and `deploy-to-slot`. Here is a high level overview of what each job does.
|
||||
|
||||
1. `build`
|
||||
1. Run the .NET restore, build, and publish commands on the project. Project is determined by the `project_build_path` input variable.
|
||||
2. Publishes built .NET artifact to GitHub artifacts. Artifact is named with the string provided in the `artifact_name` input variable.
|
||||
|
||||
2. `create-deployment-slot`
|
||||
1. Logs into the Azure CLI with [credentials provided](https://docs.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Cwindows#create-a-service-principal-and-add-it-as-a-github-secret) in the `AZURE_CREDENTIALS` secret.
|
||||
2. Runs Azure CLI command to create a new deployment slot in the Azure App Service provided in the `app_service_name` input variable. Slot name is provided via the `slot_name` input variable.
|
||||
|
||||
3. `deploy-to-slot`
|
||||
Depends on the `build` and `create-deployment-slot` jobs to succeed in order to run.
|
||||
1. Downloads the build artifact from the `build` job. Downloaded artifact name is provided via the `artifact_name` input variable.
|
||||
2. Logs into the Azure CLI with [credentials provided](https://docs.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Cwindows#create-a-service-principal-and-add-it-as-a-github-secret) in the `AZURE_CREDENTIALS` secret.
|
||||
3. Initiates deployment of the downloaded artifact to the app service and slot specified in `app_service_name` and `slot_name` respectively using the [azure/webapps-deploy](https://github.com/Azure/webapps-deploy) GitHub Action.
|
||||
|
||||
### **Swap Staging Slot into Production**
|
||||
|
||||
Workflow File: `template-pr-merge.yml`
|
||||
|
||||
Used By:
|
||||
|
||||
- `pr-merge-saas-admin.yml`
|
||||
- `pr-merge-saas-application.yml`
|
||||
- `pr-merge-saas-permissions.yml`
|
||||
- `pr-merge-saas-signupadministration.yml`
|
||||
|
||||
Triggered On: `Pull request close`
|
||||
|
||||
#### Input Variables
|
||||
|
||||
| Input Name | Description | Default |
|
||||
|---------------------------------|---------------------------------------------------------------------------------------------------------------------|------------------------------------------|
|
||||
| app_service_name | The name of the app service the slot is deployed to | |
|
||||
| app_service_resource_group_name | The name of the resource group the app service resides in | |
|
||||
| slot_name | The name of the deployment slot the staged code is deployed to (Used for override, recommended to keep the default) | pr-${{github.event.pull_request.number}} |
|
||||
|
||||
#### Secrets
|
||||
|
||||
- `AZURE_CREDENTIALS`
|
||||
- The CI/CD pipeline must contain Azure credentials for a service principal that has an appropriate access level to perform the swap operation on the Azure App Service in question. See [this](https://docs.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Cwindows) document for setting up these credentials in your own repo.
|
||||
|
||||
#### Job Breakdown
|
||||
|
||||
There are 2 main jobs within this workflow: `swap-slot` and `delete-slot`
|
||||
|
||||
1. `swap-slot` - *This job only runs if the PR is closed AND merged. This job does not run if the PR is closed without merging.*
|
||||
1. Logs into the Azure CLI with [credentials provided](https://docs.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Cwindows#create-a-service-principal-and-add-it-as-a-github-secret) in the `AZURE_CREDENTIALS` secret.
|
||||
2. Runs the Azure CLI command to swap the deployment slot named in the `slot_name` input variable with the production slot on the Azure App Service named in the `app_service_name` input variable.
|
||||
|
||||
2. `delete-slot` - This job will only run if the preceding step runs and succeeds. If the PR is closed without merging, the previous step will be skipped but this will still run.
|
||||
1. Logs into the Azure CLI with [credentials provided](https://docs.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Cwindows#create-a-service-principal-and-add-it-as-a-github-secret) in the `AZURE_CREDENTIALS` secret.
|
||||
2. Runs the Azure CLI command to delete the deployment slot named in the `slot_name` input variable with the production slot on the Azure App Service named in the `app_service_name` input variable.
|
||||
|
||||
> **Important**: You may choose to not delete the deployment slot directly following a deployment to retain the ability to swap the slot back if there are any issues and you'd like to undo the deployment. Deleting the slot immediately after a deployment is most reccomended in a development/test environment where you may be deploying multiple times per day.
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Terminology"
|
||||
linkTitle: "Terminology"
|
||||
weight: 40
|
||||
description: ""
|
||||
---
|
||||
|
||||
| Term | Description | Example |
|
||||
| ------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| SaaS Vendor or ISV | The entity that owns the application and code and sells the SaaS product. | Contoso Inc. |
|
||||
| Solution | The application the SaaS Vendor is selling | Contoso BadgeMeUp |
|
||||
| Tenant | Instance of the said application that one can purchase/subscribe to | Contoso Stadium, Joe's Coffee Shop, Lucerne Publishing |
|
||||
| Global Admin | People who work for the SaaS Vendor that has access to see all data across the solution | Jane from Contoso Operations |
|
||||
| Tenant Admin | People who purchase or administer an instance of the application | Phil from Lucerne Publishing accounting, Joe, owner of Joe's coffee shop |
|
||||
| Customer | People who use the application | Sandy employee of Lucerne Publishing, Kathy, patron of Joe's coffee shop |
|
||||
| User | Term includes Vendor Admin, Subscription Admin, or Customer. Used to describe everyone. | Jane, Joe, Adam, Phil, Sandy, and Kathy are all users |
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
type: docs
|
||||
title: "B2C Authentication Service"
|
||||
---
|
||||
|
||||
```mermaid
|
||||
graph
|
||||
user("fa:fa-user Contoso Business Admin")
|
||||
adminweb("Saas.Admin.Web")
|
||||
identityapi("fa:fa-key Auth Provider")
|
||||
catalogapi("Saas.Catalog.Api")
|
||||
catalogsql[(Saas.Catalog.Sql)]
|
||||
|
||||
user-- Bearer Token -->adminweb
|
||||
adminweb-->user
|
||||
|
||||
adminweb-- Token -->catalogapi
|
||||
catalogapi-->adminweb
|
||||
|
||||
catalogapi-->identityapi
|
||||
|
||||
catalogapi-- EF CRUD -->catalogsql
|
||||
```
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Core App"
|
||||
---
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Saas.Application"
|
||||
---
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
type: docs
|
||||
title: "Terminology"
|
||||
linkTitle: "Terminology"
|
||||
weight: 40
|
||||
description: ""
|
||||
---
|
||||
|
||||
| Term | Description | Example |
|
||||
| ------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| SaaS Vendor | The entity that owns the application and code etc. | Contoso Inc. |
|
||||
| Solution | The thing they are selling | Contoso Tickets |
|
||||
| Tenant | Instance of the said application that one can purchase/subscribe | Contoso Stadium, TD Garden, Joe's Coffee Shop |
|
||||
| Vendor Admin | People who work for the company that owns the application | Jane from Contoso accounting |
|
||||
| Tenant Admin | People who purchase or administer an instance of the application | Joe, owner of Joe's coffee shop. Adam, event coordinator of Joe's coffee shop |
|
||||
| Customer | People who use the application | Kathy, patron of Joe's coffee shop |
|
||||
| User | Term includes Vendor Admin, Subscription Admin, or Customer. Used to describe everyone. | Jane, Joe, Adam, and Kathy are all users |
|
После Ширина: | Высота: | Размер: 54 KiB |
После Ширина: | Высота: | Размер: 150 KiB |
После Ширина: | Высота: | Размер: 42 KiB |
После Ширина: | Высота: | Размер: 66 KiB |
После Ширина: | Высота: | Размер: 71 KiB |
После Ширина: | Высота: | Размер: 89 KiB |
До Ширина: | Высота: | Размер: 150 KiB После Ширина: | Высота: | Размер: 150 KiB |
До Ширина: | Высота: | Размер: 124 KiB После Ширина: | Высота: | Размер: 124 KiB |
До Ширина: | Высота: | Размер: 193 KiB После Ширина: | Высота: | Размер: 193 KiB |
После Ширина: | Высота: | Размер: 60 KiB |
После Ширина: | Высота: | Размер: 42 KiB |
|
@ -76,7 +76,7 @@
|
|||
|
||||
.td-max-width-on-larger-screens {
|
||||
@include media-breakpoint-up(lg) {
|
||||
max-width: 80%;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
}
|
|
@ -38,6 +38,10 @@
|
|||
$(() => {
|
||||
$(".language-mermaid").wrapInner("<div class=\"mermaid\"></div>");
|
||||
mermaid.init();
|
||||
|
||||
//Fixes an issue with padding above/below charts
|
||||
const svg = $(".mermaid svg");
|
||||
svg.removeAttr('height');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -281,8 +281,7 @@ public class TenantsController : ControllerBase
|
|||
/// Add a set of permissions for a user on a tenant
|
||||
/// </summary>
|
||||
/// <param name="tenantId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="permissions"></param>
|
||||
/// <param name="userEmail"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("{tenantId}/invite")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
|
|
|
@ -45,6 +45,7 @@ builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration,
|
|||
|
||||
builder.Services.AddClaimToRoleTransformer(builder.Configuration, "ClaimToRoleTransformer");
|
||||
builder.Services.AddRouteBasedRoleHandler("tenantId");
|
||||
builder.Services.AddRouteBasedRoleHandler("userId");
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
|
|
|
@ -45,7 +45,7 @@ Default values for non secret app settings can be found in [appsettings.json](Sa
|
|||
| AzureAdB2C:SignedOutCallbackPath | | false | /signout/B2C_1A_SIGNUP_SIGNIN |
|
||||
| AzureAdB2C:SignUpSignInPolicyId | | false | B2C_1A_SIGNUP_SIGNIN |
|
||||
| KeyVault:Url | KeyVault URL to pull secret values from in production | false | |
|
||||
| KeyVault:PermissionsApiCertName | Certificate name in Key Vault to use for authentication to permissions API | false | |
|
||||
| KeyVault:PermissionsApiCert | The name of the secret in Azure Key Vault that contains a base64 encoded certificate to use for authentication with the permissions api | false | |
|
||||
| PermissionsApi:BaseUrl | URL for downstream [Permissions API](../Saas.Identity/Saas.Permissions/readme.md) | false | |
|
||||
| PermissionsApi:LocalCertificate | A Base64 encoded certificate (.CER) used to authenticate with the permissions API. Only used for local development. | true | |
|
||||
| ConnectionStrings:TenantsContext | Connection String to SQL server database used to store tenants data. If using local db for development, this connection string is fine to commit to your repo as it does not contain any secrets. | true | (local db connection string) |
|
||||
|
@ -74,4 +74,4 @@ If, for your use case, you would like to deploy just this single module, you may
|
|||
<!-- TODO: Put instructions in for running bicep deploy -->
|
||||
3. Run bicep deploy command(s)
|
||||
|
||||
4. ....
|
||||
4. ....
|
||||
|
|
|
@ -5,6 +5,7 @@ using Microsoft.IdentityModel.Logging;
|
|||
using Saas.Application.Web;
|
||||
using Saas.Application.Web.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
@ -64,6 +65,13 @@ builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration,
|
|||
|
||||
builder.Services.AddControllersWithViews().AddMicrosoftIdentityUI();
|
||||
|
||||
// This is required for auth to work correctly when running in a docker container because of SSL Termination
|
||||
// Remove this and the subsequent app.UseForwardedHeaders() line below if you choose to run the app without using containers
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
|
||||
options.ForwardedProtoHeaderName = "X-Forwarded-Proto";
|
||||
});
|
||||
// Configuring appsettings section AzureAdB2C, into IOptions
|
||||
builder.Services.AddOptions();
|
||||
builder.Services.Configure<OpenIdConnectOptions>(builder.Configuration.GetSection(SR.AzureAdB2CProperty));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//----------------------
|
||||
// <auto-generated>
|
||||
// Generated using the NSwag toolchain v13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org)
|
||||
// Generated using the NSwag toolchain v13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
|
||||
// </auto-generated>
|
||||
//
|
||||
// Manual Modifications:
|
||||
|
@ -8,8 +8,6 @@
|
|||
//
|
||||
//----------------------
|
||||
|
||||
#nullable enable
|
||||
|
||||
#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?'
|
||||
|
@ -21,10 +19,9 @@
|
|||
|
||||
namespace Saas.Application.Web.Services;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using System = global::System;
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial interface IAdminServiceClient
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -47,7 +44,7 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>Created</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest? body);
|
||||
System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest body);
|
||||
|
||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <summary>
|
||||
|
@ -55,7 +52,7 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>Created</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest? body, System.Threading.CancellationToken cancellationToken);
|
||||
System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest body, System.Threading.CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get a tenant by tenant ID
|
||||
|
@ -79,7 +76,7 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO? body);
|
||||
System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO body);
|
||||
|
||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <summary>
|
||||
|
@ -87,7 +84,7 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO? body, System.Threading.CancellationToken cancellationToken);
|
||||
System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO body, System.Threading.CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a tenant
|
||||
|
@ -139,7 +136,7 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body);
|
||||
System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body);
|
||||
|
||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <summary>
|
||||
|
@ -147,14 +144,14 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body, System.Threading.CancellationToken cancellationToken);
|
||||
System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body, System.Threading.CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a set of permissions for a user on a tenant
|
||||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body);
|
||||
System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body);
|
||||
|
||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <summary>
|
||||
|
@ -162,14 +159,14 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body, System.Threading.CancellationToken cancellationToken);
|
||||
System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body, System.Threading.CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Add a set of permissions for a user on a tenant
|
||||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task InviteAsync(string tenantId, string? userEmail);
|
||||
System.Threading.Tasks.Task InviteAsync(string tenantId, string userEmail);
|
||||
|
||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <summary>
|
||||
|
@ -177,7 +174,7 @@ public partial interface IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task InviteAsync(string tenantId, string? userEmail, System.Threading.CancellationToken cancellationToken);
|
||||
System.Threading.Tasks.Task InviteAsync(string tenantId, string userEmail, System.Threading.CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get all tenant IDs that a user has access to
|
||||
|
@ -185,7 +182,7 @@ public partial interface IAdminServiceClient
|
|||
/// <param name="filter">Optionally filter by access type</param>
|
||||
/// <returns>Success</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string? filter);
|
||||
System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string filter);
|
||||
|
||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
/// <summary>
|
||||
|
@ -194,7 +191,7 @@ public partial interface IAdminServiceClient
|
|||
/// <param name="filter">Optionally filter by access type</param>
|
||||
/// <returns>Success</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string? filter, System.Threading.CancellationToken cancellationToken);
|
||||
System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string filter, System.Threading.CancellationToken cancellationToken);
|
||||
|
||||
/// <returns>Success</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
|
@ -207,20 +204,13 @@ public partial interface IAdminServiceClient
|
|||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
||||
{
|
||||
private System.Net.Http.HttpClient _httpClient;
|
||||
private System.Lazy<System.Text.Json.JsonSerializerOptions> _settings;
|
||||
|
||||
/// <summary>
|
||||
/// This has been manually modified to include an IOptions input.
|
||||
/// </summary>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="tokenAcquisition"></param>
|
||||
/// <param name="httpClient"></param>
|
||||
public AdminServiceClient(IOptions<IAdminClientSettings> configuration, ITokenAcquisition tokenAcquisition, System.Net.Http.HttpClient httpClient)
|
||||
: base(tokenAcquisition, configuration)
|
||||
public AdminServiceClient(IOptions<AppSettings> configuration, ITokenAcquisition tokenAcquisition, System.Net.Http.HttpClient httpClient) : base(tokenAcquisition, configuration)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_settings = new System.Lazy<System.Text.Json.JsonSerializerOptions>(CreateSerializerSettings);
|
||||
|
@ -342,7 +332,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>Created</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest? body)
|
||||
public virtual System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest body)
|
||||
{
|
||||
return TenantsPOSTAsync(body, System.Threading.CancellationToken.None);
|
||||
}
|
||||
|
@ -353,7 +343,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>Created</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual async System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest? body, System.Threading.CancellationToken cancellationToken)
|
||||
public virtual async System.Threading.Tasks.Task<TenantDTO> TenantsPOSTAsync(NewTenantRequest body, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
var urlBuilder_ = new System.Text.StringBuilder();
|
||||
urlBuilder_.Append("api/Tenants");
|
||||
|
@ -557,7 +547,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO? body)
|
||||
public virtual System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO body)
|
||||
{
|
||||
return TenantsPUTAsync(tenantId, body, System.Threading.CancellationToken.None);
|
||||
}
|
||||
|
@ -568,7 +558,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual async System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO? body, System.Threading.CancellationToken cancellationToken)
|
||||
public virtual async System.Threading.Tasks.Task TenantsPUTAsync(System.Guid tenantId, TenantDTO body, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
if (tenantId == null)
|
||||
throw new System.ArgumentNullException("tenantId");
|
||||
|
@ -978,7 +968,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body)
|
||||
public virtual System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body)
|
||||
{
|
||||
return PermissionsPOSTAsync(tenantId, userId, body, System.Threading.CancellationToken.None);
|
||||
}
|
||||
|
@ -989,7 +979,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual async System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body, System.Threading.CancellationToken cancellationToken)
|
||||
public virtual async System.Threading.Tasks.Task PermissionsPOSTAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
if (tenantId == null)
|
||||
throw new System.ArgumentNullException("tenantId");
|
||||
|
@ -1083,7 +1073,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body)
|
||||
public virtual System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body)
|
||||
{
|
||||
return PermissionsDELETEAsync(tenantId, userId, body, System.Threading.CancellationToken.None);
|
||||
}
|
||||
|
@ -1094,7 +1084,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual async System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string>? body, System.Threading.CancellationToken cancellationToken)
|
||||
public virtual async System.Threading.Tasks.Task PermissionsDELETEAsync(string tenantId, string userId, System.Collections.Generic.IEnumerable<string> body, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
if (tenantId == null)
|
||||
throw new System.ArgumentNullException("tenantId");
|
||||
|
@ -1188,7 +1178,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual System.Threading.Tasks.Task InviteAsync(string tenantId, string? userEmail)
|
||||
public virtual System.Threading.Tasks.Task InviteAsync(string tenantId, string userEmail)
|
||||
{
|
||||
return InviteAsync(tenantId, userEmail, System.Threading.CancellationToken.None);
|
||||
}
|
||||
|
@ -1199,7 +1189,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// </summary>
|
||||
/// <returns>No Content</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual async System.Threading.Tasks.Task InviteAsync(string tenantId, string? userEmail, System.Threading.CancellationToken cancellationToken)
|
||||
public virtual async System.Threading.Tasks.Task InviteAsync(string tenantId, string userEmail, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
if (tenantId == null)
|
||||
throw new System.ArgumentNullException("tenantId");
|
||||
|
@ -1293,7 +1283,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// <param name="filter">Optionally filter by access type</param>
|
||||
/// <returns>Success</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string? filter)
|
||||
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string filter)
|
||||
{
|
||||
return TenantsAsync(userId, filter, System.Threading.CancellationToken.None);
|
||||
}
|
||||
|
@ -1305,7 +1295,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
/// <param name="filter">Optionally filter by access type</param>
|
||||
/// <returns>Success</returns>
|
||||
/// <exception cref="ApiException">A server side error occurred.</exception>
|
||||
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string? filter, System.Threading.CancellationToken cancellationToken)
|
||||
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<TenantDTO>> TenantsAsync(string userId, string filter, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
if (userId == null)
|
||||
throw new System.ArgumentNullException("userId");
|
||||
|
@ -1495,7 +1485,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
{
|
||||
if (response == null || response.Content == null)
|
||||
{
|
||||
return new ObjectResponseResult<T>(default(T)!, string.Empty);
|
||||
return new ObjectResponseResult<T>(default(T), string.Empty);
|
||||
}
|
||||
|
||||
if (ReadResponseAsString)
|
||||
|
@ -1504,7 +1494,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
try
|
||||
{
|
||||
var typedBody = System.Text.Json.JsonSerializer.Deserialize<T>(responseText, JsonSerializerSettings);
|
||||
return new ObjectResponseResult<T>(typedBody!, responseText);
|
||||
return new ObjectResponseResult<T>(typedBody, responseText);
|
||||
}
|
||||
catch (System.Text.Json.JsonException exception)
|
||||
{
|
||||
|
@ -1519,7 +1509,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
var typedBody = await System.Text.Json.JsonSerializer.DeserializeAsync<T>(responseStream, JsonSerializerSettings, cancellationToken).ConfigureAwait(false);
|
||||
return new ObjectResponseResult<T>(typedBody!, string.Empty);
|
||||
return new ObjectResponseResult<T>(typedBody, string.Empty);
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException exception)
|
||||
|
@ -1530,7 +1520,7 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
}
|
||||
}
|
||||
|
||||
private string ConvertToString(object? value, System.Globalization.CultureInfo cultureInfo)
|
||||
private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
|
@ -1576,45 +1566,45 @@ public partial class AdminServiceClient : OAuthBaseClient, IAdminServiceClient
|
|||
}
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class NewTenantRequest
|
||||
{
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("name")]
|
||||
public string? Name { get; set; } = default!;
|
||||
public string Name { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("route")]
|
||||
public string? Route { get; set; } = default!;
|
||||
public string Route { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("creatorEmail")]
|
||||
public string? CreatorEmail { get; set; } = default!;
|
||||
public string CreatorEmail { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("productTierId")]
|
||||
public int ProductTierId { get; set; } = default!;
|
||||
public int ProductTierId { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("categoryId")]
|
||||
public int CategoryId { get; set; } = default!;
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class ProblemDetails
|
||||
{
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("type")]
|
||||
public string? Type { get; set; } = default!;
|
||||
public string Type { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("title")]
|
||||
public string? Title { get; set; } = default!;
|
||||
public string Title { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("status")]
|
||||
public int? Status { get; set; } = default!;
|
||||
public int? Status { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("detail")]
|
||||
public string? Detail { get; set; } = default!;
|
||||
public string Detail { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("instance")]
|
||||
public string? Instance { get; set; } = default!;
|
||||
public string Instance { get; set; }
|
||||
|
||||
private System.Collections.Generic.IDictionary<string, object> _additionalProperties = new System.Collections.Generic.Dictionary<string, object>();
|
||||
|
||||
|
@ -1627,60 +1617,60 @@ public partial class ProblemDetails
|
|||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class TenantDTO
|
||||
{
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("id")]
|
||||
public System.Guid Id { get; set; } = default!;
|
||||
public System.Guid Id { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("name")]
|
||||
public string? Name { get; set; } = default!;
|
||||
public string Name { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("route")]
|
||||
public string? Route { get; set; } = default!;
|
||||
public string Route { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("productTierId")]
|
||||
public int ProductTierId { get; set; } = default!;
|
||||
public int ProductTierId { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("categoryId")]
|
||||
public int CategoryId { get; set; } = default!;
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("creatorEmail")]
|
||||
public string? CreatorEmail { get; set; } = default!;
|
||||
public string CreatorEmail { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("createdTime")]
|
||||
public System.DateTimeOffset CreatedTime { get; set; } = default!;
|
||||
public System.DateTimeOffset CreatedTime { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("version")]
|
||||
public string? Version { get; set; } = default!;
|
||||
public string Version { get; set; }
|
||||
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class UserDTO
|
||||
{
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("userId")]
|
||||
public string? UserId { get; set; } = default!;
|
||||
public string UserId { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("displayName")]
|
||||
public string? DisplayName { get; set; } = default!;
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
public partial class ApiException : System.Exception
|
||||
{
|
||||
public int StatusCode { get; private set; }
|
||||
|
||||
public string? Response { 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)
|
||||
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;
|
||||
|
@ -1694,12 +1684,12 @@ public partial class ApiException : System.Exception
|
|||
}
|
||||
}
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v11.0.0.0))")]
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")]
|
||||
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)
|
||||
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;
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"microsoft.dotnet-msidentity": {
|
||||
"version": "1.0.1",
|
||||
"commands": [
|
||||
"dotnet-msidentity"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
using DemoApplication.Models;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Saas.AspNetCore.Authorization;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DemoApplication.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly ILogger<HomeController> _logger;
|
||||
|
||||
public HomeController(ILogger<HomeController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult Privacy()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Roles = "SuperAdmin")]
|
||||
[Route("subscriptions/{subscriptionId}/SuperAdmins")]
|
||||
public IActionResult GetUsersSuperAdmin(string subscriptionId)
|
||||
{
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
|
||||
[Authorize(Roles = "SystemAdmin, SubscriptionAdmin")]
|
||||
[Route("subscriptions/{subscriptionId}/Admins")]
|
||||
public IActionResult GetUsersAdmin(string subscriptionId)
|
||||
{
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
|
||||
[Authorize(Roles = "SubscriptionAdmin")]
|
||||
[Route("subscriptions/{subscriptionId}/SubAdmins")]
|
||||
public IActionResult GetUsersSubAdmin(string subscriptionId)
|
||||
{
|
||||
if(!HttpContext.User.IsInRole(subscriptionId, "SubscriptionAdmin"))
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
|
||||
[Authorize(Roles = "SubscriptionUser")]
|
||||
[Route("subscriptions/{subscriptionId}/SubUsers")]
|
||||
public IActionResult GetUsersSubUser(string subscriptionId)
|
||||
{
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = "SuperAdminOnly")]
|
||||
[Route("subscriptions/{subscriptionId}/superAdminPolicy")]
|
||||
public IActionResult GetUsersSuperAdminPolicy(string subscriptionId)
|
||||
{
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "AdminsOnlyPolicy")]
|
||||
[Route("subscriptions/{subscriptionId}/AdminsPolicy")]
|
||||
public IActionResult GetUsersAdminPolicy(string subscriptionId)
|
||||
{
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "SubscriptionAdminOnly")]
|
||||
[Route("subscriptions/{subscriptionId}/SubAdminsPolicy")]
|
||||
public IActionResult GetUsersSubAdminPolicy(string subscriptionId)
|
||||
{
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
|
||||
[Authorize(Policy = "SubscriptionUsersOnly")]
|
||||
[Route("subscriptions/{subscriptionId}/SubUsersPolicy")]
|
||||
public IActionResult GetUsersSubUserPolicy(string subscriptionId)
|
||||
{
|
||||
return RedirectToAction("privacy");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>aspnet-DemoApplication-40FD78AC-6C3C-4B1F-A0D7-4634B188A7DD</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.5" NoWarn="NU1605" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.5" NoWarn="NU1605" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.24.1" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.24.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Saas.AspNetCore.Authorization\Saas.AspNetCore.Authorization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,9 +0,0 @@
|
|||
namespace DemoApplication.Models
|
||||
{
|
||||
public class ErrorViewModel
|
||||
{
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.Identity.Web;
|
||||
using Microsoft.Identity.Web.UI;
|
||||
|
||||
using Saas.AspNetCore.Authorization.AuthHandlers;
|
||||
using Saas.AspNetCore.Authorization.ClaimTransformers;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
|
||||
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C"));
|
||||
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddClaimToRoleTransformer(builder.Configuration, "ClaimToRoleTransformer");
|
||||
builder.Services.AddRouteBasedRoleHandler("subscriptionId");
|
||||
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.FallbackPolicy = options.DefaultPolicy;
|
||||
|
||||
options.AddPolicy("AdminsOnlyPolicy", policyBuilder =>
|
||||
{
|
||||
policyBuilder.Requirements.Add(new RolesAuthorizationRequirement(new string[] { "SubscriptionAdmin", "SystemAdmin" }));
|
||||
});
|
||||
|
||||
options.AddPolicy("SubscriptionAdminOnly", policyBuilder =>
|
||||
{
|
||||
policyBuilder.Requirements.Add(new RolesAuthorizationRequirement(new string[] { "SubscriptionAdmin" }));
|
||||
});
|
||||
|
||||
options.AddPolicy("SuperAdminOnly", policyBuilder =>
|
||||
{
|
||||
policyBuilder.Requirements.Add(new RolesAuthorizationRequirement(new string[] { "SuperAdmin" }));
|
||||
});
|
||||
|
||||
options.AddPolicy("SubscriptionUsersOnly", policyBuilder =>
|
||||
{
|
||||
policyBuilder.Requirements.Add(new RolesAuthorizationRequirement(new string[] { "SubscriptionUser" }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
builder.Services.AddControllersWithViews(options =>
|
||||
{
|
||||
var policy = new AuthorizationPolicyBuilder()
|
||||
.RequireAuthenticatedUser()
|
||||
.Build();
|
||||
options.Filters.Add(new AuthorizeFilter(policy));
|
||||
});
|
||||
builder.Services.AddRazorPages()
|
||||
.AddMicrosoftIdentityUI();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
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.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
app.MapRazorPages();
|
||||
app.MapControllers();
|
||||
app.Run();
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:63696",
|
||||
"sslPort": 44396
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"DemoApplication": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7291;http://localhost:5291",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"identityapp1": {
|
||||
"type": "identityapp"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"identityapp1": {
|
||||
"type": "identityapp.aad"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
@{
|
||||
ViewData["Title"] = "Home Page";
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Welcome</h1>
|
||||
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
@{
|
||||
ViewData["Title"] = "Privacy Policy";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
|
||||
<p>Use this page to detail your site's privacy policy.</p>
|
|
@ -1,25 +0,0 @@
|
|||
@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>
|
|
@ -1,50 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - DemoApplication</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/DemoApplication.styles.css" asp-append-version="true" />
|
||||
</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-fluid">
|
||||
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">DemoApplication</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-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="Index">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</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">
|
||||
© 2022 - DemoApplication - <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>
|
|
@ -1,48 +0,0 @@
|
|||
/* 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;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0077cc;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
line-height: 60px;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
@using System.Security.Principal
|
||||
|
||||
<ul class="navbar-nav">
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
<span class="navbar-text text-dark">Hello @User.Identity?.Name!</span>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
|
@ -1,2 +0,0 @@
|
|||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
|
@ -1,3 +0,0 @@
|
|||
@using DemoApplication
|
||||
@using DemoApplication.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
@ -1,3 +0,0 @@
|
|||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"AzureAd": {
|
||||
"Instance": "https://login.microsoftonline.com/",
|
||||
"Domain": "saaskit.onmicrosoft.com",
|
||||
"TenantId": "9d68116c-e8ee-4ee6-9ab5-5c7f11b450bb",
|
||||
"ClientId": "8d4c2ba3-eb71-4326-b7f6-8901dbc9abb2",
|
||||
"CallbackPath": "/signin-oidc",
|
||||
"Scopes": "access_as_user",
|
||||
"SignedOutCallbackPath": "/signout-callback-oidc"
|
||||
},
|
||||
"AzureAdB2C": {
|
||||
"Instance": "https://saaskit.b2clogin.com",
|
||||
"ClientId": "8d4c2ba3-eb71-4326-b7f6-8901dbc9abb2",
|
||||
"Domain": "saaskit.onmicrosoft.com",
|
||||
"SignedOutCallbackPath": "/signout/B2C_1_susi",
|
||||
"SignUpSignInPolicyId": "B2C_1_susi",
|
||||
"ResetPasswordPolicyId": "B2C_1_reset",
|
||||
"EditProfilePolicyId": "B2C_1_edit_profile" // Optional profile editing policy
|
||||
//"CallbackPath": "/signin/B2C_1_sign_up_in" // defaults to /signin-oidc
|
||||
},
|
||||
"ClaimToRoleTransformer": {
|
||||
"SourceClaimType": "extension_CustomClaim",
|
||||
"RoleClaimtype": "MyCustomRoles",
|
||||
"AuthenticationType" : "MyCustomRoleAut"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 60px;
|
||||
}
|
Двоичные данные
src/Saas.Authorization/DemoApplication/wwwroot/favicon.ico
До Ширина: | Высота: | Размер: 5.3 KiB |
|
@ -1,4 +0,0 @@
|
|||
// 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.
|
|
@ -1,22 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2021 Twitter, Inc.
|
||||
Copyright (c) 2011-2021 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.
|
|
@ -1,427 +0,0 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
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: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr /* rtl:ignore */;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
|
@ -1,8 +0,0 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}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:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[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}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
|
@ -1,424 +0,0 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
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: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr ;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
|
@ -1,8 +0,0 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}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-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[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}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
|