diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..6ab8fd73 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "0.22.1", + "commands": [ + "dotnet-csharpier" + ] + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 93aca0a9..6c5d1762 100644 --- a/.gitignore +++ b/.gitignore @@ -360,3 +360,4 @@ deploy/clouds/digital-ocean/.terraform.lock.hcl deploy/k8s/.terraform.tfstate.lock.info deploy/k8s/terraform.tfstate.backup deploy/k8s/terraform.tfstate +src/WebApps/pfr-app/pfr-app/dist diff --git a/.gitmodules b/.gitmodules index a9c6c5a9..b0cc0395 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "src/SDKs/O2.Toolkit"] path = src/SDKs/O2.Toolkit - url = https://github.com/O2-Bionics/O2.Toolkit.git + url = https://github.com/O2-Bionics/O2.Toolkit.git \ No newline at end of file diff --git a/deploy/azure/azure-terraform/deploy-commands.sh b/deploy/azure/aa-old-renew/deploy-commands.sh similarity index 100% rename from deploy/azure/azure-terraform/deploy-commands.sh rename to deploy/azure/aa-old-renew/deploy-commands.sh diff --git a/deploy/azure/aa-old-renew/dev.terraform.tfvars b/deploy/azure/aa-old-renew/dev.terraform.tfvars new file mode 100644 index 00000000..45c55358 --- /dev/null +++ b/deploy/azure/aa-old-renew/dev.terraform.tfvars @@ -0,0 +1,21 @@ +k8s_resource_group = "development" +k8s_location = "westus3" + +# CLUSTER K8S +k8s_cluster_name = "o2nextgen-dev" +k8s_dns_prefix = "aks" +k8s_vm_size = "Standard_B2ms" +k8s_node_count = 1 + +#ACR +k8s_acr_name = "o2bus" + +#DNS zones +k8s_primary_dns_zone_name="o2bus.com" +# k8s_second_dns_zone_name = "pfr-centr.com" +# k8s_third_dns_zone_name = "o2bionics.com" +k8s_external_dns_name="external-dns-dev" + +# Monitoring +grafana_admin_user = "grafana" +grafana_admin_password = "grafana-pass" \ No newline at end of file diff --git a/deploy/azure/aa-old-renew/main.tf b/deploy/azure/aa-old-renew/main.tf new file mode 100644 index 00000000..57010c1d --- /dev/null +++ b/deploy/azure/aa-old-renew/main.tf @@ -0,0 +1,447 @@ +provider "helm" { + kubernetes { + host = azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.host + client_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.cluster_ca_certificate) + } +} + +provider "kubernetes" { + host = azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.host + client_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.cluster_ca_certificate) +} + +# Configure the Azure Active Directory Provider +provider "azuread" { + # subscription_id="f1404c6e-2728-40ae-9cd2-fee75bde4c04" + # tenant_id = "f3a52f65-e3a4-4386-8bc9-a42f32fc1cd6" +} +# Configure the Microsoft Azure Provider +provider "azurerm" { + features {} +} +provider "tls" {} +# We strongly recommend using the required_providers block to set the +# Azure Provider source and version being used +terraform { + backend "azure" { + } + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.0.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 2.15.0" + } + kubernetes = { + source = "kubernetes" + version = "=2.8.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.5.1" + } + + random = { + source = "hashicorp/random" + } + time = { + source = "hashicorp/time" + } + } +} + +# current subscription +data "azurerm_subscription" "current" {} + +# # current client +data "azuread_client_config" "current" {} + +output "current_subscription_display_name" { + value = data.azurerm_subscription.current.display_name +} + +output "object_id" { + value = data.azuread_client_config.current.object_id +} + +# ========================================== RESOURCE ========================================== +resource "azurerm_resource_group" "aks-resource-group" { + name = var.k8s_resource_group + location = var.k8s_location +} + +# ========================================== K8S ========================================== +resource "azurerm_kubernetes_cluster" "o2nextgen-aks" { + name = var.k8s_cluster_name + resource_group_name = var.k8s_resource_group + location = var.k8s_location + dns_prefix = var.k8s_dns_prefix + + default_node_pool { + name = "system" + node_count = var.k8s_node_count + vm_size = var.k8s_vm_size + type = "VirtualMachineScaleSets" + enable_auto_scaling = false + } + identity { + type = "SystemAssigned" + } + azure_policy_enabled = true + # # network_profile { + # load_balancer_sku = "Standard" + # network_plugin = "kubenet" # azure (CNI) + # } + + tags = { + Environment = "Production" + Product = "O2NextGen Platform" + } + + depends_on = [ + azurerm_resource_group.aks-resource-group + ] +} + +# ========================================== ACR ========================================== +# ========================================================================================= +resource "azurerm_role_assignment" "role-acrpull" { + scope = azurerm_container_registry.o2nextgen-aks-acr.id + role_definition_name = "AcrPull" + principal_id = azurerm_kubernetes_cluster.o2nextgen-aks.kubelet_identity.0.object_id + depends_on = [ + azurerm_container_registry.o2nextgen-aks-acr, + azurerm_kubernetes_cluster.o2nextgen-aks + ] +} +resource "azurerm_container_registry" "o2nextgen-aks-acr" { + name = var.k8s_acr_name + resource_group_name = var.k8s_resource_group + location = var.k8s_location + sku = "Standard" + admin_enabled = false + depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks + ] +} + +# ============================= AKS PREP - DNS ZONE in AKS ================================ +# ========================================================================================= +resource "azurerm_dns_zone" "primary-dns-zone" { + depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks + ] + name = var.k8s_primary_dns_zone_name + resource_group_name = var.k8s_resource_group + + tags = { + "type_product" = "Saas" + "product" = "O2NextGen Platform" + } +} + +resource "azurerm_dns_zone" "second-dns-zone" { +depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks +] + name = var.k8s_second_dns_zone_name + resource_group_name = var.k8s_resource_group + + tags = { + "type" = "client" + "type_product" = "Saas" + "product" = "O2NextGen Platform" + } +} +resource "azurerm_dns_zone" "third-dns-zone" { +depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks +] + name = var.k8s_third_dns_zone_name + resource_group_name = var.k8s_resource_group + + tags = { + "type" = "offsite" + "type_product" = "Saas" + "product" = "O2NextGen Platform" + } +} + +# =========================================== DNS ========================================= +# ========================================================================================= +# Create an application +resource "azuread_application" "example" { + depends_on = [ + azurerm_dns_zone.primary-dns-zone + ] + display_name = var.k8s_external_dns_name + owners = [data.azuread_client_config.current.object_id] +} + +# Create Service Principal linked to the Application +resource "azuread_service_principal" "current" { + application_id = azuread_application.example.application_id + app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] +} + +resource "azuread_application_password" "current" { + display_name = "rbac" + application_object_id = azuread_application.example.object_id +} + +# Create role assignment for service principal +# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment +resource "azurerm_role_assignment" "current" { + scope = data.azurerm_subscription.current.id + role_definition_name = "Contributor" + + # When assigning to a SP, use the object_id, not the appId + # see: https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-cli + principal_id = azuread_service_principal.current.object_id +} + +# Create role assignment for service principal +resource "azurerm_role_assignment" "main" { + scope = azurerm_dns_zone.primary-dns-zone.id + role_definition_name = "DNS Zone Contributor" + principal_id = azuread_service_principal.current.object_id +} +# Create role assignment for service principal +resource "azurerm_role_assignment" "main-second" { + scope = azurerm_dns_zone.second-dns-zone.id + role_definition_name = "DNS Zone Contributor" + principal_id = azuread_service_principal.current.object_id +} +resource "azurerm_role_assignment" "main-third" { + scope = azurerm_dns_zone.third-dns-zone.id + role_definition_name = "DNS Zone Contributor" + principal_id = azuread_service_principal.current.object_id +} + + +# Create role assignment for service principal ??? +resource "azurerm_role_assignment" "reader" { + scope = azurerm_resource_group.aks-resource-group.id + role_definition_name = "Reader" + principal_id = azuread_service_principal.current.object_id +} + +output "display_name" { + value = azuread_service_principal.current.display_name +} + +output "client_id" { + value = azuread_application.example.application_id +} + +output "client_secret" { + value = azuread_application_password.current.value + sensitive = true +} + +# ============================= MONITORING TOOLS ========================================== +# ========================================================================================= +resource "helm_release" "aad-pod-identity" { + depends_on = [ + azurerm_resource_group.aks-resource-group, + azurerm_kubernetes_cluster.o2nextgen-aks, + azurerm_container_registry.o2nextgen-aks-acr + ] + name = "aad-pod-identity" + repository = "https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts" + chart = "aad-pod-identity" + namespace = "kube-system" +} +# https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx +resource "helm_release" "nginx-ingress-controller" { + name = "nginx-ingress-controller" + repository = "https://kubernetes.github.io/ingress-nginx" + chart = "ingress-nginx" + version = "4.1.3"w + namespace = "ingress" + create_namespace = "true" + + set { + name = "controller.service.type" + value = "LoadBalancer" + } + set { + name = "controller.autoscaling.enabled" + value = "true" + } + set { + name = "controller.autoscaling.minReplicas" + value = "1" + } + set { + name = "controller.autoscaling.maxReplicas" + value = "1" + } +} + +# https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack +resource "helm_release" "prometheus-stack" { + depends_on = [ + helm_release.nginx-ingress-controller + ] + name = "prometheus-stack" + repository = "https://prometheus-community.github.io/helm-charts" + chart = "kube-prometheus-stack" + namespace = "monitoring" + create_namespace = true + + set { + name = "grafana.ingress.enabled" + value = "true" + } + set { + name = "grafana.ingress.ingressClassName" + value = "nginx" + } + # set { + # name = "server.ingress.pathType" + # value = "Prefix" + # } + set { + name = "grafana.ingress.path" + value = "/(.*)" # "/grafana2/?(.*)" + } + # annotations: + # nginx.ingress.kubernetes.io/ssl-redirect: "false" + # nginx.ingress.kubernetes.io/use-regex: "true" + # nginx.ingress.kubernetes.io/rewrite-target: /$1 + set { + name = "grafana.ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/ssl-redirect" + value = "false" + type = "string" + } + set { + name = "grafana.ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/use-regex" + value = "true" + type = "string" + } + set { + name = "grafana.ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/rewrite-target" + value = "/$1" + } + set { + name = "grafana.adminUser" + value = var.grafana_admin_user + } + set { + name = "grafana.adminPassword" + value = var.grafana_admin_password + } +} + +# ============================= External DNS ========================================== +# ========================================================================================= +resource "helm_release" "external-dns" { + depends_on = [ + azurerm_dns_zone.primary-dns-zone + ] + dependency_update = "true" + name = "external-dns" + repository = "https://charts.bitnami.com/bitnami" + chart = "external-dns" + namespace = "external-dns" + create_namespace = true + # values = [ + # local.dnsValues + # ] + set { + name = "azure.cloud" + value = "AzurePublicCloud" + } + set { + name = "txtOwnerId" + value = azurerm_kubernetes_cluster.o2nextgen-aks.name + } + set { + name = "provider" + value = "azure" + } + set { + name = "logLevel" + value = "debug" + } + set { + name = "policy" + value = "sync" + } + set { + name = "domainFilters" + value = "{${azurerm_dns_zone.primary-dns-zone.name},${azurerm_dns_zone.second-dns-zone.name},${azurerm_dns_zone.third-dns-zone.name}}" + } + set { + name = "azure.resourceGroup" + value = azurerm_kubernetes_cluster.o2nextgen-aks.resource_group_name //var.k8s_resource_group //"AzureDNS" //var.k8s_resource_group //"AzureDNS" // + } + set { + name = "azure.tenantId" + value = data.azuread_client_config.current.tenant_id + } + set { + name = "azure.subscriptionId" + value = data.azurerm_subscription.current.subscription_id + } + set { + name = "azure.aadClientId" + value = azuread_application.example.application_id + } + set { + name = "azure.aadClientSecret" + value = azuread_application_password.current.value + } +} + +# ============================= AKS PREP - Namespaces in AKS ============================== +# ========================================================================================= +resource "kubernetes_namespace" "prod" { + metadata { + annotations = { + name = "apps-prod" + } + + labels = { + Environment = "Production" + } + name = "apps-prod" + } +} + +# ==================================== SSL in AKS ========================================= +# ========================================================================================= +resource "helm_release" "cert-manager" { + name = "cert-manager" + namespace = "cert-manager" + repository = "https://charts.jetstack.io" + chart = "cert-manager" + version = "1.6.1" + create_namespace = true + set { + name = "installCRDs" + value = "true" + } + # set { + # name = "domainFilters" + # value = "{${azurerm_dns_zone.primary-dns-zone.name}}" + # } +} + +# ==================================== APPS in AKS ======================================== +# ========================================================================================= +# resource "helm_release" "o2bionics-webapp" { +# name = "o2bionics-webapp" +# namespace = "apps-prod" +# repository = "nginx" +# chart = "nginx" +# version = "latest" +# } \ No newline at end of file diff --git a/deploy/azure/aa-old-renew/prod.terraform.tfvars b/deploy/azure/aa-old-renew/prod.terraform.tfvars new file mode 100644 index 00000000..6679224a --- /dev/null +++ b/deploy/azure/aa-old-renew/prod.terraform.tfvars @@ -0,0 +1,21 @@ +k8s_resource_group = "production" +k8s_location = "westus3" + +# CLUSTER K8S +k8s_cluster_name = "o2nextgen-prod" +k8s_dns_prefix = "aks" +k8s_vm_size = "Standard_B2ms" +k8s_node_count = 1 + +#ACR +k8s_acr_name = "o2nextgen" + +#DNS zones +k8s_primary_dns_zone_name = "o2nextgen.com" +k8s_second_dns_zone_name = "pfr-centr.com" +k8s_third_dns_zone_name = "o2bionics.com" +k8s_external_dns_name = "external-dns" + +# Monitoring +grafana_admin_user = "grafana" +grafana_admin_password = "grafana-pass" diff --git a/deploy/azure/aa-old-renew/readme-fix.md b/deploy/azure/aa-old-renew/readme-fix.md new file mode 100644 index 00000000..bb7d2cd1 --- /dev/null +++ b/deploy/azure/aa-old-renew/readme-fix.md @@ -0,0 +1,2 @@ +command for fix certificates +kubectl describe clusterissuer letsencrypt \ No newline at end of file diff --git a/deploy/azure/aa-old-renew/variables.tf b/deploy/azure/aa-old-renew/variables.tf new file mode 100644 index 00000000..fee30e2b --- /dev/null +++ b/deploy/azure/aa-old-renew/variables.tf @@ -0,0 +1,61 @@ +variable "k8s_resource_group" { + type = string + description = "Resourse group for AKS cluster" +} + +variable "k8s_location" { + type = string + description = "Resourse group location for AKS cluster" +} + +variable "k8s_cluster_name" { + type = string + description = "AKS cluster name" +} + +variable "k8s_dns_prefix" { + type = string + description = "DNS prefix for AKS Cluster" +} + +variable "k8s_vm_size" { + type = string + description = "Name VM for AKS Cluster" +} + +variable "k8s_node_count" { + type = number + description = "Node count for AKS Cluster" +} + +variable "k8s_acr_name" { + type = string + description = "Name for ACR" +} +variable "k8s_primary_dns_zone_name" { + type = string + description = "Name PRIMARY DNS ZONE for AKS Cluster" +} +variable "k8s_second_dns_zone_name" { + type = string + description = "Name PRIMARY DNS ZONE for AKS Cluster" +} +variable "k8s_third_dns_zone_name" { + type = string + description = "Name PRIMARY DNS ZONE for AKS Cluster" +} + +variable "k8s_external_dns_name" { + type = string + description = "Name principal for PRIMARY DNS ZONE of AKS Cluster" +} + +variable "grafana_admin_user" { + type = string + description = "Admin user to access Grafana dashboard" +} + +variable "grafana_admin_password" { + type = string + description = "Admin password to access Grafana dashboard" +} diff --git a/deploy/azure/aa-old/create-role-for-certmanager.sh b/deploy/azure/aa-old/create-role-for-certmanager.sh new file mode 100644 index 00000000..b1ebb6e2 --- /dev/null +++ b/deploy/azure/aa-old/create-role-for-certmanager.sh @@ -0,0 +1,17 @@ +# Choose a name for the service principal that contacts azure DNS to present the challenge +$ AZURE_CERT_MANAGER_NEW_SP_NAME=NEW_SERVICE_PRINCIPAL_NAME +# This is the name of the resource group that you have your dns zone in +$ AZURE_DNS_ZONE_RESOURCE_GROUP=AZURE_DNS_ZONE_RESOURCE_GROUP +# The DNS zone name. It should be something like domain.com or sub.domain.com +$ AZURE_DNS_ZONE=AZURE_DNS_ZONE + +$ DNS_SP=$(az ad sp create-for-rbac --name $AZURE_CERT_MANAGER_NEW_SP_NAME) +$ AZURE_CERT_MANAGER_SP_APP_ID=$(echo $DNS_SP | jq -r '.appId') +$ AZURE_CERT_MANAGER_SP_PASSWORD=$(echo $DNS_SP | jq -r '.password') +$ AZURE_TENANT_ID=$(echo $DNS_SP | jq -r '.tenant') +$ AZURE_SUBSCRIPTION_ID=$(az account show | jq -r '.id') + +$ az role assignment delete --assignee $AZURE_CERT_MANAGER_SP_APP_ID --role Contributor + +read here +https://cert-manager.io/v0.14-docs/configuration/acme/dns01/azuredns/ \ No newline at end of file diff --git a/deploy/azure/azure-terraform/delete-infastructure.sh b/deploy/azure/aa-old/delete-infastructure.sh similarity index 100% rename from deploy/azure/azure-terraform/delete-infastructure.sh rename to deploy/azure/aa-old/delete-infastructure.sh diff --git a/deploy/azure/aa-old/deploy-commands.sh b/deploy/azure/aa-old/deploy-commands.sh new file mode 100644 index 00000000..8cc32aa1 --- /dev/null +++ b/deploy/azure/aa-old/deploy-commands.sh @@ -0,0 +1,45 @@ +# make sure terraform CLI is installed +terraform + +# format the tf files +terraform fmt + +# initialize terraform Azure modules +terraform init + +# validate the template +terraform validate + +# plan and save the infra changes into tfplan file +terraform plan -out tfplan + +# show the tfplan file +terraform show -json tfplan +terraform show -json tfplan >> tfplan.json + +# Format tfplan.json file +terraform show -json tfplan | jq '.' > tfplan.json + +# show only the changes +cat tfplan.json | jq -r '(.resource_changes[] | [.change.actions[], .type, .change.after.name]) | @tsv' +cat tfplan.json | jq '[.resource_changes[] | {type: .type, name: .change.after.name, actions: .change.actions[]}]' + +# apply the infra changes +terraform apply tfplan + + +read -t 240 -p "Press [Enter] key to start remove all resources..." + + +# initialize terraform Azure modules +terraform init + +# delete the infra +terraform destroy -auto-approve + +# cleanup files +rm terraform.tfstate +rm terraform.tfstate.backup +rm tfplan +rm tfplan.json +rm -r .terraform/ \ No newline at end of file diff --git a/deploy/azure/azure-terraform/deploy-infastructure.sh b/deploy/azure/aa-old/deploy-infastructure.sh similarity index 100% rename from deploy/azure/azure-terraform/deploy-infastructure.sh rename to deploy/azure/aa-old/deploy-infastructure.sh diff --git a/deploy/azure/aa-old/dev.terraform.tfvars b/deploy/azure/aa-old/dev.terraform.tfvars new file mode 100644 index 00000000..54b2f6d3 --- /dev/null +++ b/deploy/azure/aa-old/dev.terraform.tfvars @@ -0,0 +1,14 @@ +k8s_cluster_name = "o2nextgen-aks" +k8s_resource_group = "develop" +k8s_location = "centralus" +k8s_dns_prefix = "aks" +k8s_vm_size = "Standard_B2ms" +k8s_node_count = 1 +k8s_vm_pool2_size = "Standard_B2ms" +k8s_node_pool2_count = 1 +o2nextgen-aks +grafana_admin_user = "grafana" +grafana_admin_password = "grafana-pass" +k8s_acr_name = "o2nextgen" +keyvault_name = "o2nextgen-keyvault-dev" +# harbor_admin_password = "Harbor12345" # "harbor-pass" diff --git a/deploy/azure/aa-old/main.tf b/deploy/azure/aa-old/main.tf new file mode 100644 index 00000000..3e94d0dd --- /dev/null +++ b/deploy/azure/aa-old/main.tf @@ -0,0 +1,687 @@ + + +provider "helm" { + kubernetes { + host = azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.host + client_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.cluster_ca_certificate) + } +} + +provider "kubernetes" { + host = azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.host + client_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.o2nextgen-aks.kube_config.0.cluster_ca_certificate) +} + +# Configure the Azure Active Directory Provider +provider "azuread" { + # subscription_id="f1404c6e-2728-40ae-9cd2-fee75bde4c04" + # tenant_id = "f3a52f65-e3a4-4386-8bc9-a42f32fc1cd6" +} +# Configure the Microsoft Azure Provider +provider "azurerm" { + features {} +} +provider "tls" {} +# We strongly recommend using the required_providers block to set the +# Azure Provider source and version being used +terraform { + backend "azure" { + } + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.0.0" + } + kubernetes = { + source = "kubernetes" + version = "=2.8.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.5.1" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 2.15.0" + } + random = { + source = "hashicorp/random" + } + time = { + source = "hashicorp/time" + } + } +} + +# ========================================== RESOURCE ========================================== +resource "azurerm_resource_group" "aks-resource-group" { + name = var.k8s_resource_group + location = var.k8s_location +} + +# ========================================== K8S ========================================== +resource "azurerm_kubernetes_cluster" "o2nextgen-aks" { + name = var.k8s_cluster_name + resource_group_name = var.k8s_resource_group + location = var.k8s_location + dns_prefix = var.k8s_dns_prefix + + default_node_pool { + name = "system" + node_count = var.k8s_node_count + vm_size = var.k8s_vm_size + type = "VirtualMachineScaleSets" + enable_auto_scaling = false + } + identity { + type = "SystemAssigned" + } + azure_policy_enabled = true + # # network_profile { + # load_balancer_sku = "Standard" + # network_plugin = "kubenet" # azure (CNI) + # } + + tags = { + Environment = "Production" + Product = "O2NextGen Platform" + } + + depends_on = [ + azurerm_resource_group.aks-resource-group + ] +} + + +# ========================================== ACR ========================================== +# ========================================================================================= +resource "azurerm_role_assignment" "role-acrpull" { + scope = azurerm_container_registry.o2nextgen-aks-acr.id + role_definition_name = "AcrPull" + principal_id = azurerm_kubernetes_cluster.o2nextgen-aks.kubelet_identity.0.object_id + depends_on = [ + azurerm_container_registry.o2nextgen-aks-acr, + azurerm_kubernetes_cluster.o2nextgen-aks + ] +} +resource "azurerm_container_registry" "o2nextgen-aks-acr" { + name = var.k8s_acr_name + resource_group_name = var.k8s_resource_group + location = var.k8s_location + sku = "Standard" + admin_enabled = false + depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks + ] +} + +# ============================= AKS PREP - DNS ZONE in AKS ================================ +# ========================================================================================= +resource "azurerm_dns_zone" "primary-dns-zone" { + depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks + ] + name = "o2bus.com" + resource_group_name = var.k8s_resource_group + + tags = { + "type_product" = "Saas" + "product" = "O2NextGen Platform" + } +} +resource "azurerm_dns_zone" "second-dns-zone" { +depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks +] + name = "pfr-centr.com" + resource_group_name = var.k8s_resource_group + + tags = { + "type" = "client" + "type_product" = "Saas" + "product" = "O2NextGen Platform" + } +} +resource "azurerm_dns_zone" "third-dns-zone" { +depends_on = [ + azurerm_kubernetes_cluster.o2nextgen-aks +] + name = "o2bionics.com" + resource_group_name = var.k8s_resource_group + + tags = { + "type" = "offsite" + "type_product" = "Saas" + "product" = "O2NextGen Platform" + } +} + +# resource "azuread_application" "example" { +# display_name = "sp-external-dns" +# owners = [data.azuread_client_config.current.object_id] +# } +# resource "azuread_service_principal" "sp-external-dns" { +# principal_id = azuread_service_principal.sp-external-dns.template_id +# } + +# current subscription +data "azurerm_subscription" "current" {} + +# # current client +data "azuread_client_config" "current" {} + +output "current_subscription_display_name" { + value = data.azurerm_subscription.current.display_name +} + +output "object_id" { + value = data.azuread_client_config.current.object_id +} + +# Create an application +resource "azuread_application" "example" { + depends_on = [ + azurerm_dns_zone.primary-dns-zone + ] + display_name = "external-dns" + owners = [data.azuread_client_config.current.object_id] +} + +# # Create a service principal +# resource "azuread_service_principal" "example" { +# application_id = azuread_application.example.application_id +# owners = [data.azuread_client_config.current.object_id] +# } + +# Create Service Principal linked to the Application +resource "azuread_service_principal" "current" { + application_id = azuread_application.example.application_id + app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] +} + +# # Create role assignment for Service Principal +# resource "azurerm_role_assignment" "contributor" { +# scope = data.azurerm_subscription.current.id +# role_definition_name = "Contributor" +# principal_id = azuread_service_principal.current.id +# } + +# # # Generate random string to be used for Service Principal password +# resource "random_string" "password" { +# length = 32 +# special = true +# } +resource "azuread_application_password" "current" { + display_name = "rbac" + application_object_id = azuread_application.example.object_id +} + +# off azurerm_key_vault +# resource "azurerm_key_vault" "keyvault" { +# name = var.keyvault_name +# location = var.k8s_location +# resource_group_name = var.k8s_resource_group +# enabled_for_disk_encryption = false +# tenant_id = data.azuread_client_config.current.tenant_id +# soft_delete_retention_days = 7 +# purge_protection_enabled = false + +# sku_name = "standard" + +# access_policy { +# tenant_id = data.azuread_client_config.current.tenant_id +# object_id = data.azuread_client_config.current.object_id + +# key_permissions = [ +# "Get", +# ] + +# secret_permissions = [ +# "Get", +# "List", +# "Set", +# "Delete" +# ] + +# storage_permissions = [ +# "Get", +# ] +# } + +# # network_acls { +# # default_action = "Deny" # "Allow" +# # bypass = "AzureServices" # "None" +# # ip_rules = ["50.50.50.50/24"] +# # } +# } +# resource "time_rotating" "example" { +# rotation_days = 7 +# } +# # Create Service Principal password +# resource "azuread_service_principal_password" "main" { +# service_principal_id = azuread_service_principal.current.object_id +# rotate_when_changed = { +# rotation = time_rotating.example.id +# } +# } + +# Create role assignment for service principal +# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment +resource "azurerm_role_assignment" "current" { + scope = data.azurerm_subscription.current.id + role_definition_name = "Contributor" + + # When assigning to a SP, use the object_id, not the appId + # see: https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-cli + principal_id = azuread_service_principal.current.object_id +} + +# Create role assignment for service principal +resource "azurerm_role_assignment" "main" { + scope = azurerm_dns_zone.primary-dns-zone.id + role_definition_name = "DNS Zone Contributor" + principal_id = azuread_service_principal.current.object_id +} +# Create role assignment for service principal +resource "azurerm_role_assignment" "main-second" { + scope = azurerm_dns_zone.second-dns-zone.id + role_definition_name = "DNS Zone Contributor" + principal_id = azuread_service_principal.current.object_id +} +resource "azurerm_role_assignment" "main-third" { + scope = azurerm_dns_zone.third-dns-zone.id + role_definition_name = "DNS Zone Contributor" + principal_id = azuread_service_principal.current.object_id +} + +# Create role assignment for service principal +resource "azurerm_role_assignment" "reader" { + scope = azurerm_resource_group.aks-resource-group.id + role_definition_name = "Reader" + principal_id = azuread_service_principal.current.object_id +} + +output "display_name" { + value = azuread_service_principal.current.display_name +} + +output "client_id" { + value = azuread_application.example.application_id +} + +output "client_secret" { + value = azuread_application_password.current.value + sensitive = true +} + + +# # Generate a private key +# resource "tls_private_key" "example" { +# algorithm = "RSA" +# rsa_bits = 2048 +# } + +# # Generate a self signed certificate +# resource "tls_self_signed_cert" "example" { +# private_key_pem = tls_private_key.example.private_key_pem + +# subject { +# common_name = azuread_application.example.display_name +# organization = "O2 Bionics LLC" +# } + +# allowed_uses = ["client_auth", "server_auth"] +# validity_period_hours = 8760 +# } + +# # Create Application certificate (client certificate) +# resource "azuread_application_certificate" "example" { +# application_object_id = azuread_application.example.object_id +# type = "AsymmetricX509Cert" +# end_date_relative = "4320h" # expire in 6 months +# value = tls_self_signed_cert.example.cert_pem +# } + +# # Create Application password (client secret) +# resource "azuread_application_password" "example" { +# application_object_id = azuread_application.example.object_id +# end_date_relative = "4320h" # expire in 6 months +# } +# output "account_id" { +# value = data.azurerm_client_config.current.service_principal_application_id +# } + +# resource "azuread_application" "example" { +# display_name = "External-DNS-SP" +# owners = [data.azurerm_client_config.current.object_id] +# } + +# resource "azuread_service_principal" "example" { +# application_id = azuread_application.example.application_id +# app_role_assignment_required = false +# owners = [data.azurerm_client_config.current.object_id] +# } + +# data "azuread_application_template" "example" { +# display_name = "External-DNS-SP" +# } + +# resource "azuread_application" "example" { +# display_name = "sp-external-dns" +# template_id = data.azuread_application_template.example.template_id +# } + +# resource "azuread_service_principal" "example" { +# application_id = azuread_application.example.application_id +# use_existing = true +# } + +# resource "azurerm_role_assignment" "role-secret-officer" { +# role_definition_name = "Key Vault Secrets Officer" +# principal_id = data.azurerm_client_config.current.object_id +# scope = azurerm_key_vault.keyvault.id +# } +# ============================= MONITORING TOOLS ========================================== +# ========================================================================================= +resource "helm_release" "aad-pod-identity" { + depends_on = [ + azurerm_resource_group.aks-resource-group, + azurerm_kubernetes_cluster.o2nextgen-aks, + azurerm_container_registry.o2nextgen-aks-acr + ] + name = "aad-pod-identity" + repository = "https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts" + chart = "aad-pod-identity" + namespace = "kube-system" +} + +# https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx +resource "helm_release" "nginx-ingress-controller" { + name = "nginx-ingress-controller" + repository = "https://kubernetes.github.io/ingress-nginx" + chart = "ingress-nginx" + version = "4.1.3" + namespace = "ingress" + create_namespace = "true" + + set { + name = "controller.service.type" + value = "LoadBalancer" + } + set { + name = "controller.autoscaling.enabled" + value = "true" + } + set { + name = "controller.autoscaling.minReplicas" + value = "1" + } + set { + name = "controller.autoscaling.maxReplicas" + value = "1" + } +} + +# https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack +resource "helm_release" "prometheus-stack" { + depends_on = [ + helm_release.nginx-ingress-controller + ] + name = "prometheus-stack" + repository = "https://prometheus-community.github.io/helm-charts" + chart = "kube-prometheus-stack" + namespace = "monitoring" + create_namespace = true + + set { + name = "grafana.ingress.enabled" + value = "true" + } + set { + name = "grafana.ingress.ingressClassName" + value = "nginx" + } + # set { + # name = "server.ingress.pathType" + # value = "Prefix" + # } + set { + name = "grafana.ingress.path" + value = "/(.*)" # "/grafana2/?(.*)" + } + # annotations: + # nginx.ingress.kubernetes.io/ssl-redirect: "false" + # nginx.ingress.kubernetes.io/use-regex: "true" + # nginx.ingress.kubernetes.io/rewrite-target: /$1 + set { + name = "grafana.ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/ssl-redirect" + value = "false" + type = "string" + } + set { + name = "grafana.ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/use-regex" + value = "true" + type = "string" + } + set { + name = "grafana.ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/rewrite-target" + value = "/$1" + } + set { + name = "grafana.adminUser" + value = var.grafana_admin_user + } + set { + name = "grafana.adminPassword" + value = var.grafana_admin_password + } +} +locals { + dnsValues = <> tfplan.json + +# # Format tfplan.json file +# terraform show -json tfplan -var-file=./dev/dev.tfvars| jq '.' > tfplan.json + +# # show only the changes +# cat tfplan.json | jq -r '(.resource_changes[] | [.change.actions[], .type, .change.after.name]) | @tsv' +# cat tfplan.json | jq '[.resource_changes[] | {type: .type, name: .change.after.name, actions: .change.actions[]}]' diff --git a/deploy/azure/azure-terraform/variables.tf b/deploy/azure/azure-terraform/variables.tf index 1806f5e6..86cea95e 100644 --- a/deploy/azure/azure-terraform/variables.tf +++ b/deploy/azure/azure-terraform/variables.tf @@ -7,6 +7,16 @@ variable "k8s_location" { type = string description = "Resourse group location for AKS cluster" } +variable "k8s_env" { + type = string + description = "env" +} + +variable "k8s_version" { + type = string + description = "Kubernetes version" +} + variable "k8s_cluster_name" { type = string description = "AKS cluster name" @@ -22,12 +32,41 @@ variable "k8s_vm_size" { description = "Name VM for AKS Cluster" } - variable "k8s_node_count" { type = number description = "Node count for AKS Cluster" } +variable "dns_primary_zone_name" { + type = string + description = "main DNS zone" +} + +variable "dns_second_dns_zone_name" { + type = string + description = "main DNS zone" +} + +variable "dns_third_dns_zone_name" { + type = string + description = "main DNS zone" +} + +variable "dns_resource_group" { + type = string + description = "DNS resource group" +} + +variable "dns_location" { + type = string + description = "DNS location" +} + +variable "k8s_acr_name" { + type = string + description = "Name for ACR" +} + variable "grafana_admin_user" { type = string description = "Admin user to access Grafana dashboard" @@ -36,4 +75,20 @@ variable "grafana_admin_user" { variable "grafana_admin_password" { type = string description = "Admin password to access Grafana dashboard" +} + +variable "harbor_admin_password" { + type = string + description = "Admin password to access Harbor dashboard" +} + + +variable "storage_account_name" { + type = string + description = "Storage Account name in Azure" +} + +variable "storage_container_name" { + type = string + description = "Storage Container name in Azure" } \ No newline at end of file diff --git a/deploy/helm/auth-web/.helmignore b/deploy/helm/auth-web/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/deploy/helm/auth-web/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/helm/auth-web/Chart.yaml b/deploy/helm/auth-web/Chart.yaml new file mode 100644 index 00000000..7e78cbac --- /dev/null +++ b/deploy/helm/auth-web/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: auth-web +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/deploy/helm/auth-web/dev.values.yaml b/deploy/helm/auth-web/dev.values.yaml new file mode 100644 index 00000000..70bcba17 --- /dev/null +++ b/deploy/helm/auth-web/dev.values.yaml @@ -0,0 +1,91 @@ +# Default values for auth-web. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/proxy-buffering: "on" + nginx.ingress.kubernetes.io/proxy-body-size: 16m + nginx.ingress.kubernetes.io/proxy-buffer-size: 8k + nginx.ingress.kubernetes.io/proxy-busy-buffers-size: 16k + nginx.ingress.kubernetes.io/client-body-buffer-size: 16m + hosts: + - host: auth.o2bus.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-secret-auth-webapp + hosts: + - auth.o2bus.com + +env: + values: + - name: ConnectionString + value: Server=tcp:mssql-data;Initial Catalog=O2Bionics.O2NextGen.IdServerDb;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30; +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/auth-web/templates/NOTES.txt b/deploy/helm/auth-web/templates/NOTES.txt new file mode 100644 index 00000000..66d98933 --- /dev/null +++ b/deploy/helm/auth-web/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "auth-web.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "auth-web.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "auth-web.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "auth-web.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/helm/auth-web/templates/_helpers.tpl b/deploy/helm/auth-web/templates/_helpers.tpl new file mode 100644 index 00000000..38d9e621 --- /dev/null +++ b/deploy/helm/auth-web/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "auth-web.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "auth-web.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "auth-web.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "auth-web.labels" -}} +helm.sh/chart: {{ include "auth-web.chart" . }} +{{ include "auth-web.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "auth-web.selectorLabels" -}} +app.kubernetes.io/name: {{ include "auth-web.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "auth-web.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "auth-web.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/auth-web/templates/deployment.yaml b/deploy/helm/auth-web/templates/deployment.yaml new file mode 100644 index 00000000..69bce32d --- /dev/null +++ b/deploy/helm/auth-web/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "auth-web.fullname" . }} + labels: + {{- include "auth-web.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "auth-web.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "auth-web.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "auth-web.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- if .Values.env.values -}} + {{- range .Values.env.values }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: /account/login + port: http + readinessProbe: + httpGet: + path: /account/login + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/auth-web/templates/hpa.yaml b/deploy/helm/auth-web/templates/hpa.yaml new file mode 100644 index 00000000..76f25d4f --- /dev/null +++ b/deploy/helm/auth-web/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "auth-web.fullname" . }} + labels: + {{- include "auth-web.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "auth-web.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deploy/helm/auth-web/templates/ingress.yaml b/deploy/helm/auth-web/templates/ingress.yaml new file mode 100644 index 00000000..4fde63c8 --- /dev/null +++ b/deploy/helm/auth-web/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "auth-web.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "auth-web.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/auth-web/templates/service.yaml b/deploy/helm/auth-web/templates/service.yaml new file mode 100644 index 00000000..616a06f7 --- /dev/null +++ b/deploy/helm/auth-web/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "auth-web.fullname" . }} + labels: + {{- include "auth-web.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "auth-web.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/auth-web/templates/serviceaccount.yaml b/deploy/helm/auth-web/templates/serviceaccount.yaml new file mode 100644 index 00000000..1677c5b5 --- /dev/null +++ b/deploy/helm/auth-web/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "auth-web.serviceAccountName" . }} + labels: + {{- include "auth-web.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/auth-web/templates/tests/test-connection.yaml b/deploy/helm/auth-web/templates/tests/test-connection.yaml new file mode 100644 index 00000000..87c66f6f --- /dev/null +++ b/deploy/helm/auth-web/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "auth-web.fullname" . }}-test-connection" + labels: + {{- include "auth-web.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "auth-web.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deploy/helm/auth-web/values.yaml b/deploy/helm/auth-web/values.yaml new file mode 100644 index 00000000..03a801b3 --- /dev/null +++ b/deploy/helm/auth-web/values.yaml @@ -0,0 +1,98 @@ +# Default values for auth-web. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" + # nginx.ingress.kubernetes.io/proxy-buffering: "on" + # nginx.ingress.kubernetes.io/proxy-body-size: 16m + # nginx.ingress.kubernetes.io/proxy-buffer-size: 8k + # nginx.ingress.kubernetes.io/proxy-busy-buffers-size: 16k + # nginx.ingress.kubernetes.io/client-body-buffer-size: 16m + # nginx.ingress.kubernetes.io/large-client-header-buffers: 4 100k + # nginx.ingress.kubernetes.io/proxy-read-timeout: "1000" + # nginx.ingress.kubernetes.io/proxy-send-timeout: "1000" + hosts: + - host: auth.o2nextgen.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-secret-auth-webapp + hosts: + - auth.o2nextgen.com + +env: + values: + - name: ConnectionString + value: Server=tcp:mssql-data;Initial Catalog=O2Bionics.O2NextGen.IdServerDb;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30; + - name: Urls__PfrMvcUrl + value: https://app.pfr-centr.com + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/cgen-api/.helmignore b/deploy/helm/cgen-api/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/deploy/helm/cgen-api/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/helm/cgen-api/Chart.yaml b/deploy/helm/cgen-api/Chart.yaml new file mode 100644 index 00000000..84cc1efa --- /dev/null +++ b/deploy/helm/cgen-api/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: cgen-api +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/deploy/helm/cgen-api/dev.values.yaml b/deploy/helm/cgen-api/dev.values.yaml new file mode 100644 index 00000000..4d048849 --- /dev/null +++ b/deploy/helm/cgen-api/dev.values.yaml @@ -0,0 +1,98 @@ +# Default values for cgen-api. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + kubernetes.io/ingress.global-static-ip-name: webapp-ipv6 + # acme.cert-manager.io/http01-edit-in-place: "true" + hosts: + - host: cgen-api.o2bus.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-o2bus-cgen-api + hosts: + - cgen-api.o2bus.com +env: + values: + - name: ConnectionString + value: Server=tcp:mssql-data;Initial Catalog=O2Bionics.O2NextGen.CGenDb;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30; + - name: DatabaseInitializerSettings__Initialize + value: false +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +health: + livenessProbe: + httpGet: + path: /version + port: http + readinessProbe: + httpGet: + path: /version + port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/cgen-api/templates/NOTES.txt b/deploy/helm/cgen-api/templates/NOTES.txt new file mode 100644 index 00000000..72f9354f --- /dev/null +++ b/deploy/helm/cgen-api/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "cgen-api.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "cgen-api.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "cgen-api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "cgen-api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/helm/cgen-api/templates/_helpers.tpl b/deploy/helm/cgen-api/templates/_helpers.tpl new file mode 100644 index 00000000..0e559756 --- /dev/null +++ b/deploy/helm/cgen-api/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cgen-api.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cgen-api.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cgen-api.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cgen-api.labels" -}} +helm.sh/chart: {{ include "cgen-api.chart" . }} +{{ include "cgen-api.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cgen-api.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cgen-api.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cgen-api.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cgen-api.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/cgen-api/templates/deployment.yaml b/deploy/helm/cgen-api/templates/deployment.yaml new file mode 100644 index 00000000..0e9fab40 --- /dev/null +++ b/deploy/helm/cgen-api/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "cgen-api.fullname" . }} + labels: + {{- include "cgen-api.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "cgen-api.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "cgen-api.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "cgen-api.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- if .Values.env.values -}} + {{- range .Values.env.values }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: {{ .Values.health.livenessProbe.httpGet.path }} + port: {{ .Values.health.livenessProbe.httpGet.port }} + readinessProbe: + httpGet: + path: {{ .Values.health.readinessProbe.httpGet.path }} + port: {{ .Values.health.readinessProbe.httpGet.port }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/cgen-api/templates/hpa.yaml b/deploy/helm/cgen-api/templates/hpa.yaml new file mode 100644 index 00000000..85a83a7d --- /dev/null +++ b/deploy/helm/cgen-api/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "cgen-api.fullname" . }} + labels: + {{- include "cgen-api.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "cgen-api.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deploy/helm/cgen-api/templates/ingress.yaml b/deploy/helm/cgen-api/templates/ingress.yaml new file mode 100644 index 00000000..5d3be969 --- /dev/null +++ b/deploy/helm/cgen-api/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "cgen-api.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "cgen-api.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/cgen-api/templates/service.yaml b/deploy/helm/cgen-api/templates/service.yaml new file mode 100644 index 00000000..a99e0ae9 --- /dev/null +++ b/deploy/helm/cgen-api/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cgen-api.fullname" . }} + labels: + {{- include "cgen-api.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "cgen-api.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/cgen-api/templates/serviceaccount.yaml b/deploy/helm/cgen-api/templates/serviceaccount.yaml new file mode 100644 index 00000000..d719c3ae --- /dev/null +++ b/deploy/helm/cgen-api/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "cgen-api.serviceAccountName" . }} + labels: + {{- include "cgen-api.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/cgen-api/templates/tests/test-connection.yaml b/deploy/helm/cgen-api/templates/tests/test-connection.yaml new file mode 100644 index 00000000..1c855093 --- /dev/null +++ b/deploy/helm/cgen-api/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "cgen-api.fullname" . }}-test-connection" + labels: + {{- include "cgen-api.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "cgen-api.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deploy/helm/cgen-api/values.yaml b/deploy/helm/cgen-api/values.yaml new file mode 100644 index 00000000..82bb9f13 --- /dev/null +++ b/deploy/helm/cgen-api/values.yaml @@ -0,0 +1,100 @@ +# Default values for cgen-api. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + # kubernetes.io/ingress.global-static-ip-name: webapp-ipv6 + # acme.cert-manager.io/http01-edit-in-place: "true" + # nginx.ingress.kubernetes.io/rewrite-target: / + cert-manager.io/acme-challenge-type: http01 + hosts: + - host: cgen-api.o2nextgen.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-o2nextgen-cgen-api + hosts: + - cgen-api.o2nextgen.com +env: + values: + - name: ConnectionString + value: Server=tcp:mssql-data;Initial Catalog=O2Bionics.O2NextGen.CGenDb;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30; + - name: DatabaseInitializerSettings__Initialize + value: true +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi +health: + livenessProbe: + httpGet: + path: /version + port: http + readinessProbe: + httpGet: + path: /version + port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/elasticsearch-1.32.5.tgz b/deploy/helm/elasticsearch-1.32.5.tgz new file mode 100644 index 00000000..20e3139c Binary files /dev/null and b/deploy/helm/elasticsearch-1.32.5.tgz differ diff --git a/deploy/helm/elasticsearch/.helmignore b/deploy/helm/elasticsearch/.helmignore new file mode 100755 index 00000000..f2256510 --- /dev/null +++ b/deploy/helm/elasticsearch/.helmignore @@ -0,0 +1,3 @@ +.git +# OWNERS file for Kubernetes +OWNERS \ No newline at end of file diff --git a/deploy/helm/elasticsearch/Chart.yaml b/deploy/helm/elasticsearch/Chart.yaml new file mode 100755 index 00000000..d26a5df9 --- /dev/null +++ b/deploy/helm/elasticsearch/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +appVersion: 6.8.6 +deprecated: true +description: DEPRECATED Flexible and powerful open source, distributed real-time search + and analytics engine. +home: https://www.elastic.co/products/elasticsearch +icon: https://static-www.elastic.co/assets/blteb1c97719574938d/logo-elastic-elasticsearch-lt.svg +name: elasticsearch +sources: +- https://www.elastic.co/products/elasticsearch +- https://github.com/jetstack/elasticsearch-pet +- https://github.com/giantswarm/kubernetes-elastic-stack +- https://github.com/GoogleCloudPlatform/elasticsearch-docker +- https://github.com/clockworksoul/helm-elasticsearch +- https://github.com/pires/kubernetes-elasticsearch-cluster +version: 1.32.5 diff --git a/deploy/helm/elasticsearch/README.md b/deploy/helm/elasticsearch/README.md new file mode 100755 index 00000000..c7579238 --- /dev/null +++ b/deploy/helm/elasticsearch/README.md @@ -0,0 +1,278 @@ +# Elasticsearch Helm Chart + +This chart uses a standard Docker image of Elasticsearch (docker.elastic.co/elasticsearch/elasticsearch-oss) and uses a service pointing to the master's transport port for service discovery. +Elasticsearch does not communicate with the Kubernetes API, hence no need for RBAC permissions. + +## This Helm chart is deprecated +As mentioned in #10543 this chart has been deprecated in favour of the official [Elastic Helm Chart](https://github.com/elastic/helm-charts/tree/master/elasticsearch). +We have made steps towards that goal by producing a [migration guide](https://github.com/elastic/helm-charts/blob/master/elasticsearch/examples/migration/README.md) to help people switch the management of their clusters over to the new Charts. +The Elastic Helm Chart supports version 6 and 7 of Elasticsearch and it was decided it would be easier for people to upgrade after migrating to the Elastic Helm Chart because it's upgrade process works better. +During deprecation process we want to make sure that Chart will do what people are using this chart to do. +Please look at the Elastic Helm Charts and if you see anything missing from please [open an issue](https://github.com/elastic/helm-charts/issues/new/choose) to let us know what you need. +The Elastic Chart repo is also in [Helm Hub](https://hub.helm.sh). + +## Warning for previous users +If you are currently using an earlier version of this Chart you will need to redeploy your Elasticsearch clusters. The discovery method used here is incompatible with using RBAC. +If you are upgrading to Elasticsearch 6 from the 5.5 version used in this chart before, please note that your cluster needs to do a full cluster restart. +The simplest way to do that is to delete the installation (keep the PVs) and install this chart again with the new version. +If you want to avoid doing that upgrade to Elasticsearch 5.6 first before moving on to Elasticsearch 6.0. + +## Prerequisites Details + +* Kubernetes 1.10+ +* PV dynamic provisioning support on the underlying infrastructure + +## StatefulSets Details +* https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ + +## StatefulSets Caveats +* https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#limitations + +## Todo + +* Implement TLS/Auth/Security +* Smarter upscaling/downscaling +* Solution for memory locking + +## Chart Details +This chart will do the following: + +* Implemented a dynamically scalable elasticsearch cluster using Kubernetes StatefulSets/Deployments +* Multi-role deployment: master, client (coordinating) and data nodes +* Statefulset Supports scaling down without degrading the cluster + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +$ helm install --name my-release stable/elasticsearch +``` + +## Deleting the Charts + +Delete the Helm deployment as normal + +``` +$ helm delete my-release +``` + +Deletion of the StatefulSet doesn't cascade to deleting associated PVCs. To delete them: + +``` +$ kubectl delete pvc -l release=my-release,component=data +``` + +## Configuration + +The following table lists the configurable parameters of the elasticsearch chart and their default values. + +| Parameter | Description | Default | +| ------------------------------------ | ------------------------------------------------------------------- | --------------------------------------------------- | +| `appVersion` | Application Version (Elasticsearch) | `6.8.2` | +| `image.repository` | Container image name | `docker.elastic.co/elasticsearch/elasticsearch-oss` | +| `image.tag` | Container image tag | `6.8.2` | +| `image.pullPolicy` | Container pull policy | `IfNotPresent` | +| `image.pullSecrets` | container image pull secrets | `[]` | +| `initImage.repository` | Init container image name | `busybox` | +| `initImage.tag` | Init container image tag | `latest` | +| `initImage.pullPolicy` | Init container pull policy | `Always` | +| `schedulerName` | Name of the k8s scheduler (other than default) | `nil` | +| `cluster.name` | Cluster name | `elasticsearch` | +| `cluster.xpackEnable` | Writes the X-Pack configuration options to the configuration file | `false` | +| `cluster.config` | Additional cluster config appended | `{}` | +| `cluster.keystoreSecret` | Name of secret holding secure config options in an es keystore | `nil` | +| `cluster.env` | Cluster environment variables | `{MINIMUM_MASTER_NODES: "2"}` | +| `cluster.bootstrapShellCommand` | Post-init command to run in separate Job | `""` | +| `cluster.additionalJavaOpts` | Cluster parameters to be added to `ES_JAVA_OPTS` environment variable | `""` | +| `cluster.plugins` | List of Elasticsearch plugins to install | `[]` | +| `cluster.loggingYml` | Cluster logging configuration for ES v2 | see `values.yaml` for defaults | +| `cluster.log4j2Properties` | Cluster logging configuration for ES v5 and 6 | see `values.yaml` for defaults | +| `client.name` | Client component name | `client` | +| `client.replicas` | Client node replicas (deployment) | `2` | +| `client.resources` | Client node resources requests & limits | `{} - cpu limit must be an integer` | +| `client.priorityClassName` | Client priorityClass | `nil` | +| `client.heapSize` | Client node heap size | `512m` | +| `client.podAnnotations` | Client Deployment annotations | `{}` | +| `client.nodeSelector` | Node labels for client pod assignment | `{}` | +| `client.tolerations` | Client tolerations | `[]` | +| `client.terminationGracePeriodSeconds` | Client nodes: Termination grace period (seconds) | `nil` | +| `client.serviceAnnotations` | Client Service annotations | `{}` | +| `client.serviceType` | Client service type | `ClusterIP` | +| `client.httpNodePort` | Client service HTTP NodePort port number. Has no effect if client.serviceType is not `NodePort`. | `nil` | +| `client.loadBalancerIP` | Client loadBalancerIP | `{}` | +| `client.loadBalancerSourceRanges` | Client loadBalancerSourceRanges | `{}` | +| `client.antiAffinity` | Client anti-affinity policy | `soft` | +| `client.nodeAffinity` | Client node affinity policy | `{}` | +| `client.initResources` | Client initContainer resources requests & limits | `{}` | +| `client.hooks.preStop` | Client nodes: Lifecycle hook script to execute prior the pod stops | `nil` | +| `client.hooks.preStart` | Client nodes: Lifecycle hook script to execute after the pod starts | `nil` | +| `client.additionalJavaOpts` | Parameters to be added to `ES_JAVA_OPTS` environment variable for client | `""` | +| `client.ingress.enabled` | Enable Client Ingress | `false` | +| `client.ingress.user` | If this & password are set, enable basic-auth on ingress | `nil` | +| `client.ingress.password` | If this & user are set, enable basic-auth on ingress | `nil` | +| `client.ingress.annotations` | Client Ingress annotations | `{}` | +| `client.ingress.hosts` | Client Ingress Hostnames | `[]` | +| `client.ingress.tls` | Client Ingress TLS configuration | `[]` | +| `client.exposeTransportPort` | Expose transport port 9300 on client service (ClusterIP) | `false` | +| `master.initResources` | Master initContainer resources requests & limits | `{}` | +| `master.additionalJavaOpts` | Parameters to be added to `ES_JAVA_OPTS` environment variable for master | `""` | +| `master.exposeHttp` | Expose http port 9200 on master Pods for monitoring, etc | `false` | +| `master.name` | Master component name | `master` | +| `master.replicas` | Master node replicas (deployment) | `2` | +| `master.resources` | Master node resources requests & limits | `{} - cpu limit must be an integer` | +| `master.priorityClassName` | Master priorityClass | `nil` | +| `master.podAnnotations` | Master Deployment annotations | `{}` | +| `master.nodeSelector` | Node labels for master pod assignment | `{}` | +| `master.tolerations` | Master tolerations | `[]` | +| `master.terminationGracePeriodSeconds` | Master nodes: Termination grace period (seconds) | `nil` | +| `master.heapSize` | Master node heap size | `512m` | +| `master.name` | Master component name | `master` | +| `master.persistence.enabled` | Master persistent enabled/disabled | `true` | +| `master.persistence.name` | Master statefulset PVC template name | `data` | +| `master.persistence.size` | Master persistent volume size | `4Gi` | +| `master.persistence.storageClass` | Master persistent volume Class | `nil` | +| `master.persistence.accessMode` | Master persistent Access Mode | `ReadWriteOnce` | +| `master.readinessProbe` | Master container readiness probes | see `values.yaml` for defaults | +| `master.antiAffinity` | Master anti-affinity policy | `soft` | +| `master.nodeAffinity` | Master node affinity policy | `{}` | +| `master.podManagementPolicy` | Master pod creation strategy | `OrderedReady` | +| `master.updateStrategy` | Master node update strategy policy | `{type: "onDelete"}` | +| `master.hooks.preStop` | Master nodes: Lifecycle hook script to execute prior the pod stops | `nil` | +| `master.hooks.preStart` | Master nodes: Lifecycle hook script to execute after the pod starts | `nil` | +| `data.initResources` | Data initContainer resources requests & limits | `{}` | +| `data.additionalJavaOpts` | Parameters to be added to `ES_JAVA_OPTS` environment variable for data | `""` | +| `data.exposeHttp` | Expose http port 9200 on data Pods for monitoring, etc | `false` | +| `data.replicas` | Data node replicas (statefulset) | `2` | +| `data.resources` | Data node resources requests & limits | `{} - cpu limit must be an integer` | +| `data.priorityClassName` | Data priorityClass | `nil` | +| `data.heapSize` | Data node heap size | `1536m` | +| `data.hooks.drain.enabled` | Data nodes: Enable drain pre-stop and post-start hook | `true` | +| `data.hooks.preStop` | Data nodes: Lifecycle hook script to execute prior the pod stops. Ignored if `data.hooks.drain.enabled` is `true` | `nil` | +| `data.hooks.preStart` | Data nodes: Lifecycle hook script to execute after the pod starts. Ignored if `data.hooks.drain.enabled` is `true` | `nil`| +| `data.persistence.enabled` | Data persistent enabled/disabled | `true` | +| `data.persistence.name` | Data statefulset PVC template name | `data` | +| `data.persistence.size` | Data persistent volume size | `30Gi` | +| `data.persistence.storageClass` | Data persistent volume Class | `nil` | +| `data.persistence.accessMode` | Data persistent Access Mode | `ReadWriteOnce` | +| `data.readinessProbe` | Readiness probes for data-containers | see `values.yaml` for defaults | +| `data.podAnnotations` | Data StatefulSet annotations | `{}` | +| `data.nodeSelector` | Node labels for data pod assignment | `{}` | +| `data.tolerations` | Data tolerations | `[]` | +| `data.terminationGracePeriodSeconds` | Data termination grace period (seconds) | `3600` | +| `data.antiAffinity` | Data anti-affinity policy | `soft` | +| `data.nodeAffinity` | Data node affinity policy | `{}` | +| `data.podManagementPolicy` | Data pod creation strategy | `OrderedReady` | +| `data.updateStrategy` | Data node update strategy policy | `{type: "onDelete"}` | +| `sysctlInitContainer.enabled` | If true, the sysctl init container is enabled (does not stop chownInitContainer or extraInitContainers from running) | `true` | +| `chownInitContainer.enabled` | If true, the chown init container is enabled (does not stop sysctlInitContainer or extraInitContainers from running) | `true` | +| `extraInitContainers` | Additional init container passed through the tpl | `` | +| `podSecurityPolicy.annotations` | Specify pod annotations in the pod security policy | `{}` | +| `podSecurityPolicy.enabled` | Specify if a pod security policy must be created | `false` | +| `securityContext.enabled` | If true, add securityContext to client, master and data pods | `false` | +| `securityContext.runAsUser` | user ID to run containerized process | `1000` | +| `serviceAccounts.client.create` | If true, create the client service account | `true` | +| `serviceAccounts.client.name` | Name of the client service account to use or create | `{{ elasticsearch.client.fullname }}` | +| `serviceAccounts.master.create` | If true, create the master service account | `true` | +| `serviceAccounts.master.name` | Name of the master service account to use or create | `{{ elasticsearch.master.fullname }}` | +| `serviceAccounts.data.create` | If true, create the data service account | `true` | +| `serviceAccounts.data.name` | Name of the data service account to use or create | `{{ elasticsearch.data.fullname }}` | +| `testFramework.image` | `test-framework` image repository. | `dduportal/bats` | +| `testFramework.tag` | `test-framework` image tag. | `0.4.0` | +| `forceIpv6` | force to use IPv6 address to listen if set to true | `false` | + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. + +In terms of Memory resources you should make sure that you follow that equation: + +- `${role}HeapSize < ${role}MemoryRequests < ${role}MemoryLimits` + +The YAML value of cluster.config is appended to elasticsearch.yml file for additional customization ("script.inline: on" for example to allow inline scripting) + +# Deep dive + +## Application Version + +This chart aims to support Elasticsearch v2 to v6 deployments by specifying the `values.yaml` parameter `appVersion`. + +### Version Specific Features + +* Memory Locking *(variable renamed)* +* Ingest Node *(v5)* +* X-Pack Plugin *(v5)* + +Upgrade paths & more info: https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html + +## Mlocking + +This is a limitation in kubernetes right now. There is no way to raise the +limits of lockable memory, so that these memory areas won't be swapped. This +would degrade performance heavily. The issue is tracked in +[kubernetes/#3595](https://github.com/kubernetes/kubernetes/issues/3595). + +``` +[WARN ][bootstrap] Unable to lock JVM Memory: error=12,reason=Cannot allocate memory +[WARN ][bootstrap] This can result in part of the JVM being swapped out. +[WARN ][bootstrap] Increase RLIMIT_MEMLOCK, soft limit: 65536, hard limit: 65536 +``` + +## Minimum Master Nodes +> The minimum_master_nodes setting is extremely important to the stability of your cluster. This setting helps prevent split brains, the existence of two masters in a single cluster. + +>When you have a split brain, your cluster is at danger of losing data. Because the master is considered the supreme ruler of the cluster, it decides when new indices can be created, how shards are moved, and so forth. If you have two masters, data integrity becomes perilous, since you have two nodes that think they are in charge. + +>This setting tells Elasticsearch to not elect a master unless there are enough master-eligible nodes available. Only then will an election take place. + +>This setting should always be configured to a quorum (majority) of your master-eligible nodes. A quorum is (number of master-eligible nodes / 2) + 1 + +More info: https://www.elastic.co/guide/en/elasticsearch/guide/1.x/_important_configuration_changes.html#_minimum_master_nodes + +# Client and Coordinating Nodes + +Elasticsearch v5 terminology has updated, and now refers to a `Client Node` as a `Coordinating Node`. + +More info: https://www.elastic.co/guide/en/elasticsearch/reference/5.5/modules-node.html#coordinating-node + +## Enabling elasticsearch internal monitoring +Requires version 6.3+ and standard non `oss` repository defined. Starting with 6.3 Xpack is partially free and enabled by default. You need to set a new config to enable the collection of these internal metrics. (https://www.elastic.co/guide/en/elasticsearch/reference/6.3/monitoring-settings.html) + +To do this through this helm chart override with the three following changes: +``` +image.repository: docker.elastic.co/elasticsearch/elasticsearch +cluster.xpackEnable: true +cluster.env.XPACK_MONITORING_ENABLED: true +``` + +Note: to see these changes you will need to update your kibana repo to `image.repository: docker.elastic.co/kibana/kibana` instead of the `oss` version + + +## Select right storage class for SSD volumes + +### GCE + Kubernetes 1.5 + +Create StorageClass for SSD-PD + +``` +$ kubectl create -f - < /dev/null; then + echo "Plugin $PLUGIN_NAME already exists, skipping." + else + /usr/share/elasticsearch/bin/elasticsearch-plugin install -b $PLUGIN_NAME + fi + {{- end }} + volumeMounts: + - mountPath: /usr/share/elasticsearch/plugins/ + name: plugindir + - mountPath: /usr/share/elasticsearch/config/elasticsearch.yml + name: config + subPath: elasticsearch.yml +{{- end -}} diff --git a/deploy/helm/elasticsearch/templates/client-auth.yaml b/deploy/helm/elasticsearch/templates/client-auth.yaml new file mode 100755 index 00000000..08e90c71 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/client-auth.yaml @@ -0,0 +1,11 @@ +{{- if and ( .Values.client.ingress.user ) ( .Values.client.ingress.password ) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: '{{ include "elasticsearch.client.fullname" . }}-auth' +type: Opaque +data: + auth: {{ printf "%s:{PLAIN}%s\n" .Values.client.ingress.user .Values.client.ingress.password | b64enc | quote }} +{{- end }} + diff --git a/deploy/helm/elasticsearch/templates/client-deployment.yaml b/deploy/helm/elasticsearch/templates/client-deployment.yaml new file mode 100755 index 00000000..31c7d876 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/client-deployment.yaml @@ -0,0 +1,207 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.client.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.client.fullname" . }} +spec: + selector: + matchLabels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.client.name }}" + release: {{ .Release.Name }} + replicas: {{ .Values.client.replicas }} + template: + metadata: + labels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.client.name }}" + release: {{ .Release.Name }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if .Values.client.podAnnotations }} +{{ toYaml .Values.client.podAnnotations | indent 8 }} + {{- end }} + spec: + serviceAccountName: {{ template "elasticsearch.serviceAccountName.client" . }} +{{- if .Values.client.priorityClassName }} + priorityClassName: "{{ .Values.client.priorityClassName }}" +{{- end }} + securityContext: + fsGroup: 1000 + {{- if or .Values.client.antiAffinity .Values.client.nodeAffinity }} + affinity: + {{- end }} + {{- if eq .Values.client.antiAffinity "hard" }} + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchLabels: + app: "{{ template "elasticsearch.name" . }}" + release: "{{ .Release.Name }}" + component: "{{ .Values.client.name }}" + {{- else if eq .Values.client.antiAffinity "soft" }} + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app: "{{ template "elasticsearch.name" . }}" + release: "{{ .Release.Name }}" + component: "{{ .Values.client.name }}" + {{- end }} + {{- with .Values.client.nodeAffinity }} + nodeAffinity: +{{ toYaml . | indent 10 }} + {{- end }} +{{- if .Values.client.nodeSelector }} + nodeSelector: +{{ toYaml .Values.client.nodeSelector | indent 8 }} +{{- end }} +{{- if .Values.client.tolerations }} + tolerations: +{{ toYaml .Values.client.tolerations | indent 8 }} +{{- end }} +{{- if .Values.client.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ .Values.client.terminationGracePeriodSeconds }} +{{- end }} +{{- if or .Values.extraInitContainers .Values.sysctlInitContainer.enabled .Values.cluster.plugins }} + initContainers: +{{- if .Values.sysctlInitContainer.enabled }} + # see https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html + # and https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration-memory.html#mlockall + - name: "sysctl" + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: {{ .Values.initImage.pullPolicy | quote }} + resources: +{{ toYaml .Values.client.initResources | indent 12 }} + command: ["sysctl", "-w", "vm.max_map_count=262144"] + securityContext: + privileged: true +{{- end }} +{{- if .Values.extraInitContainers }} +{{ tpl .Values.extraInitContainers . | indent 6 }} +{{- end }} +{{- if .Values.cluster.plugins }} +{{ include "plugin-installer" . | indent 6 }} +{{- end }} +{{- end }} + containers: + - name: elasticsearch + env: + - name: NODE_DATA + value: "false" +{{- if hasPrefix "5." .Values.appVersion }} + - name: NODE_INGEST + value: "false" +{{- end }} + - name: NODE_MASTER + value: "false" + - name: DISCOVERY_SERVICE + value: {{ template "elasticsearch.fullname" . }}-discovery + - name: PROCESSORS + valueFrom: + resourceFieldRef: + resource: limits.cpu + - name: ES_JAVA_OPTS + value: "-Djava.net.preferIPv4Stack=true -Xms{{ .Values.client.heapSize }} -Xmx{{ .Values.client.heapSize }} {{ .Values.cluster.additionalJavaOpts }} {{ .Values.client.additionalJavaOpts }}" + {{- range $key, $value := .Values.cluster.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + resources: +{{ toYaml .Values.client.resources | indent 12 }} + readinessProbe: + httpGet: + path: /_cluster/health + port: 9200 + initialDelaySeconds: 5 + livenessProbe: + httpGet: + path: /_cluster/health?local=true + port: 9200 + initialDelaySeconds: 90 + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + {{- end }} + ports: + - containerPort: 9200 + name: http + - containerPort: 9300 + name: transport + volumeMounts: + - mountPath: /usr/share/elasticsearch/config/elasticsearch.yml + name: config + subPath: elasticsearch.yml +{{- if .Values.cluster.plugins }} + - mountPath: /usr/share/elasticsearch/plugins/ + name: plugindir +{{- end }} +{{- if hasPrefix "2." .Values.appVersion }} + - mountPath: /usr/share/elasticsearch/config/logging.yml + name: config + subPath: logging.yml +{{- end }} +{{- if hasPrefix "5." .Values.appVersion }} + - mountPath: /usr/share/elasticsearch/config/log4j2.properties + name: config + subPath: log4j2.properties +{{- end }} +{{- if .Values.cluster.keystoreSecret }} + - name: keystore + mountPath: "/usr/share/elasticsearch/config/elasticsearch.keystore" + subPath: elasticsearch.keystore + readOnly: true +{{- end }} +{{- if .Values.client.hooks.preStop }} + - name: config + mountPath: /client-pre-stop-hook.sh + subPath: client-pre-stop-hook.sh +{{- end }} +{{- if .Values.client.hooks.postStart }} + - name: config + mountPath: /client-post-start-hook.sh + subPath: client-post-start-hook.sh +{{- end }} +{{- if or .Values.client.hooks.preStop .Values.client.hooks.postStart }} + lifecycle: + {{- if .Values.client.hooks.preStop }} + preStop: + exec: + command: ["/bin/bash","/client-pre-stop-hook.sh"] + {{- end }} + {{- if .Values.client.hooks.postStart }} + postStart: + exec: + command: ["/bin/bash","/client-post-start-hook.sh"] + {{- end }} +{{- end }} +{{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range $pullSecret := .Values.image.pullSecrets }} + - name: {{ $pullSecret }} + {{- end }} +{{- end }} + volumes: + - name: config + configMap: + name: {{ template "elasticsearch.fullname" . }} +{{- if .Values.cluster.plugins }} + - name: plugindir + emptyDir: {} +{{- end }} +{{- if .Values.cluster.keystoreSecret }} + - name: keystore + secret: + secretName: {{ .Values.cluster.keystoreSecret }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/client-ingress.yaml b/deploy/helm/elasticsearch/templates/client-ingress.yaml new file mode 100755 index 00000000..66d41fea --- /dev/null +++ b/deploy/helm/elasticsearch/templates/client-ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.client.ingress.enabled -}} +{{- $fullName := include "elasticsearch.client.fullname" . -}} +{{- $ingressPath := .Values.client.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.client.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + annotations: +{{- with .Values.client.ingress.annotations }} +{{ toYaml . | indent 4 }} +{{- end }} +{{- if and ( .Values.client.ingress.user ) ( .Values.client.ingress.password ) }} + nginx.ingress.kubernetes.io/auth-type: basic + nginx.ingress.kubernetes.io/auth-secret: '{{ include "elasticsearch.client.fullname" . }}-auth' + nginx.ingress.kubernetes.io/auth-realm: "Authentication-Required" +{{- end }} +spec: +{{- if .Values.client.ingress.tls }} + tls: + {{- range .Values.client.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.client.ingress.hosts }} + - host: {{ . | quote }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/client-pdb.yaml b/deploy/helm/elasticsearch/templates/client-pdb.yaml new file mode 100755 index 00000000..ccb3f359 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/client-pdb.yaml @@ -0,0 +1,24 @@ +{{- if .Values.client.podDisruptionBudget.enabled }} +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.client.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.client.fullname" . }} +spec: +{{- if .Values.client.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.client.podDisruptionBudget.minAvailable }} +{{- end }} +{{- if .Values.client.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.client.podDisruptionBudget.maxUnavailable }} +{{- end }} + selector: + matchLabels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.client.name }}" + release: {{ .Release.Name }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/client-serviceaccount.yaml b/deploy/helm/elasticsearch/templates/client-serviceaccount.yaml new file mode 100755 index 00000000..e6476827 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/client-serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccounts.client.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.client.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.client.fullname" . }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/client-svc.yaml b/deploy/helm/elasticsearch/templates/client-svc.yaml new file mode 100755 index 00000000..5fbbc18c --- /dev/null +++ b/deploy/helm/elasticsearch/templates/client-svc.yaml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.client.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.client.fullname" . }} +{{- if .Values.client.serviceAnnotations }} + annotations: +{{ toYaml .Values.client.serviceAnnotations | indent 4 }} +{{- end }} + +spec: + ports: + - name: http + port: 9200 +{{- if and .Values.client.httpNodePort (eq .Values.client.serviceType "NodePort") }} + nodePort: {{ .Values.client.httpNodePort }} +{{- end }} + targetPort: http +{{- if .Values.client.exposeTransportPort }} + - name: transport + port: 9300 + targetPort: transport +{{- end }} + selector: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.client.name }}" + release: {{ .Release.Name }} + type: {{ .Values.client.serviceType }} +{{- if .Values.client.loadBalancerIP }} + loadBalancerIP: "{{ .Values.client.loadBalancerIP }}" +{{- end }} + {{if .Values.client.loadBalancerSourceRanges}} + loadBalancerSourceRanges: + {{range $rangeList := .Values.client.loadBalancerSourceRanges}} + - {{ $rangeList }} + {{end}} + {{end}} \ No newline at end of file diff --git a/deploy/helm/elasticsearch/templates/configmap.yaml b/deploy/helm/elasticsearch/templates/configmap.yaml new file mode 100755 index 00000000..493346ed --- /dev/null +++ b/deploy/helm/elasticsearch/templates/configmap.yaml @@ -0,0 +1,168 @@ +{{ $minorAppVersion := regexFind "[0-9]*.[0-9]*" .Values.appVersion | float64 -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "elasticsearch.fullname" . }} + labels: + app: {{ template "elasticsearch.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +data: + elasticsearch.yml: |- + cluster.name: {{ .Values.cluster.name }} + + node.data: ${NODE_DATA:true} + node.master: ${NODE_MASTER:true} +{{- if hasPrefix "5." .Values.appVersion }} + node.ingest: ${NODE_INGEST:true} +{{- else if hasPrefix "6." .Values.appVersion }} + node.ingest: ${NODE_INGEST:true} +{{- end }} + node.name: ${HOSTNAME} + +{{- if .Values.forceIpv6 }} + network.host: "::" +{{- else }} + network.host: 0.0.0.0 +{{- end }} + +{{- if hasPrefix "2." .Values.appVersion }} + # see https://github.com/kubernetes/kubernetes/issues/3595 + bootstrap.mlockall: ${BOOTSTRAP_MLOCKALL:false} + + discovery: + zen: + ping.unicast.hosts: ${DISCOVERY_SERVICE:} + minimum_master_nodes: ${MINIMUM_MASTER_NODES:2} +{{- else if hasPrefix "5." .Values.appVersion }} + # see https://github.com/kubernetes/kubernetes/issues/3595 + bootstrap.memory_lock: ${BOOTSTRAP_MEMORY_LOCK:false} + + discovery: + zen: + ping.unicast.hosts: ${DISCOVERY_SERVICE:} + minimum_master_nodes: ${MINIMUM_MASTER_NODES:2} + +{{- if .Values.cluster.xpackEnable }} + # see https://www.elastic.co/guide/en/x-pack/current/xpack-settings.html + {{- if or ( gt $minorAppVersion 5.4 ) ( eq $minorAppVersion 5.4 ) }} + xpack.ml.enabled: ${XPACK_ML_ENABLED:false} + {{- end }} + xpack.monitoring.enabled: ${XPACK_MONITORING_ENABLED:false} + xpack.security.enabled: ${XPACK_SECURITY_ENABLED:false} + xpack.watcher.enabled: ${XPACK_WATCHER_ENABLED:false} +{{- else }} + {{- if or ( gt $minorAppVersion 5.4 ) ( eq $minorAppVersion 5.4 ) }} + xpack.ml.enabled: false + {{- end }} + xpack.monitoring.enabled: false + xpack.security.enabled: false + xpack.watcher.enabled: false +{{- end }} +{{- else if hasPrefix "6." .Values.appVersion }} + # see https://github.com/kubernetes/kubernetes/issues/3595 + bootstrap.memory_lock: ${BOOTSTRAP_MEMORY_LOCK:false} + + discovery: + zen: + ping.unicast.hosts: ${DISCOVERY_SERVICE:} + minimum_master_nodes: ${MINIMUM_MASTER_NODES:2} + +{{- if and ( .Values.cluster.xpackEnable ) ( gt $minorAppVersion 6.3 ) }} + # see https://www.elastic.co/guide/en/x-pack/current/xpack-settings.html + # After 6.3 xpack systems changed and are enabled by default and different configs manage them this enables monitoring + xpack.monitoring.collection.enabled: ${XPACK_MONITORING_ENABLED:false} +{{- else if .Values.cluster.xpackEnable }} + # see https://www.elastic.co/guide/en/x-pack/current/xpack-settings.html + xpack.ml.enabled: ${XPACK_ML_ENABLED:false} + xpack.monitoring.enabled: ${XPACK_MONITORING_ENABLED:false} + xpack.security.enabled: ${XPACK_SECURITY_ENABLED:false} + xpack.watcher.enabled: ${XPACK_WATCHER_ENABLED:false} +{{- end }} +{{- end }} + + # see https://github.com/elastic/elasticsearch-definitive-guide/pull/679 + processors: ${PROCESSORS:} + + # avoid split-brain w/ a minimum consensus of two masters plus a data node + gateway.expected_master_nodes: ${EXPECTED_MASTER_NODES:2} + gateway.expected_data_nodes: ${EXPECTED_DATA_NODES:1} + gateway.recover_after_time: ${RECOVER_AFTER_TIME:5m} + gateway.recover_after_master_nodes: ${RECOVER_AFTER_MASTER_NODES:2} + gateway.recover_after_data_nodes: ${RECOVER_AFTER_DATA_NODES:1} +{{- with .Values.cluster.config }} +{{ toYaml . | indent 4 }} +{{- end }} +{{- if hasPrefix "2." .Values.appVersion }} + logging.yml: |- +{{ toYaml .Values.cluster.loggingYml | indent 4 }} +{{- else }} + log4j2.properties: |- +{{ tpl .Values.cluster.log4j2Properties . | indent 4 }} +{{- end }} +{{- if .Values.data.hooks.drain.enabled }} + data-pre-stop-hook.sh: |- + #!/bin/bash + exec &> >(tee -a "/var/log/elasticsearch-hooks.log") + NODE_NAME=${HOSTNAME} + echo "Prepare to migrate data of the node ${NODE_NAME}" + echo "Move all data from node ${NODE_NAME}" + curl -s -XPUT -H 'Content-Type: application/json' '{{ template "elasticsearch.client.fullname" . }}:9200/_cluster/settings' -d "{ + \"transient\" :{ + \"cluster.routing.allocation.exclude._name\" : \"${NODE_NAME}\" + } + }" + echo "" + + while true ; do + echo -e "Wait for node ${NODE_NAME} to become empty" + SHARDS_ALLOCATION=$(curl -s -XGET 'http://{{ template "elasticsearch.client.fullname" . }}:9200/_cat/shards') + if ! echo "${SHARDS_ALLOCATION}" | grep -E "${NODE_NAME}"; then + break + fi + sleep 1 + done + echo "Node ${NODE_NAME} is ready to shutdown" + data-post-start-hook.sh: |- + #!/bin/bash + exec &> >(tee -a "/var/log/elasticsearch-hooks.log") + NODE_NAME=${HOSTNAME} + CLUSTER_SETTINGS=$(curl -s -XGET "http://{{ template "elasticsearch.client.fullname" . }}:9200/_cluster/settings") + if echo "${CLUSTER_SETTINGS}" | grep -E "${NODE_NAME}"; then + echo "Activate node ${NODE_NAME}" + curl -s -XPUT -H 'Content-Type: application/json' "http://{{ template "elasticsearch.client.fullname" . }}:9200/_cluster/settings" -d "{ + \"transient\" :{ + \"cluster.routing.allocation.exclude._name\" : null + } + }" + fi + echo "Node ${NODE_NAME} is ready to be used" +{{- else }} + {{- if .Values.data.hooks.preStop }} + data-pre-stop-hook.sh: |- +{{ tpl .Values.data.hooks.preStop . | indent 4 }} + {{- end }} + {{- if .Values.data.hooks.postStart }} + data-post-start-hook.sh: |- +{{ tpl .Values.data.hooks.postStart . | indent 4 }} + {{- end }} +{{- end }} + +{{- if .Values.client.hooks.preStop }} + client-pre-stop-hook.sh: |- +{{ tpl .Values.client.hooks.preStop . | indent 4 }} +{{- end }} +{{- if .Values.client.hooks.postStart }} + client-post-start-hook.sh: |- +{{ tpl .Values.client.hooks.postStart . | indent 4 }} +{{- end }} + +{{- if .Values.master.hooks.preStop }} + master-pre-stop-hook.sh: |- +{{ tpl .Values.master.hooks.preStop . | indent 4 }} +{{- end }} +{{- if .Values.master.hooks.postStart }} + master-post-start-hook.sh: |- +{{ tpl .Values.master.hooks.postStart . | indent 4 }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/data-pdb.yaml b/deploy/helm/elasticsearch/templates/data-pdb.yaml new file mode 100755 index 00000000..54e91c75 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/data-pdb.yaml @@ -0,0 +1,24 @@ +{{- if .Values.data.podDisruptionBudget.enabled }} +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.data.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.data.fullname" . }} +spec: +{{- if .Values.data.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.data.podDisruptionBudget.minAvailable }} +{{- end }} +{{- if .Values.data.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.data.podDisruptionBudget.maxUnavailable }} +{{- end }} + selector: + matchLabels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.data.name }}" + release: {{ .Release.Name }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/data-serviceaccount.yaml b/deploy/helm/elasticsearch/templates/data-serviceaccount.yaml new file mode 100755 index 00000000..2a9b4fd3 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/data-serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccounts.data.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.data.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.data.fullname" . }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/data-statefulset.yaml b/deploy/helm/elasticsearch/templates/data-statefulset.yaml new file mode 100755 index 00000000..d2990e18 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/data-statefulset.yaml @@ -0,0 +1,256 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.data.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.data.fullname" . }} +spec: + selector: + matchLabels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.data.name }}" + release: {{ .Release.Name }} + role: data + serviceName: {{ template "elasticsearch.data.fullname" . }} + replicas: {{ .Values.data.replicas }} + template: + metadata: + labels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.data.name }}" + release: {{ .Release.Name }} + role: data +{{- if or .Values.data.podAnnotations (eq .Values.data.updateStrategy.type "RollingUpdate") }} + annotations: + {{- if .Values.data.podAnnotations }} +{{ toYaml .Values.data.podAnnotations | indent 8 }} + {{- end }} + {{- if eq .Values.data.updateStrategy.type "RollingUpdate" }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- end }} +{{- end }} + spec: + {{- if .Values.schedulerName }} + schedulerName: "{{ .Values.schedulerName }}" + {{- end }} + serviceAccountName: {{ template "elasticsearch.serviceAccountName.data" . }} +{{- if .Values.data.priorityClassName }} + priorityClassName: "{{ .Values.data.priorityClassName }}" +{{- end }} + securityContext: + fsGroup: 1000 + {{- if or .Values.data.antiAffinity .Values.data.nodeAffinity }} + affinity: + {{- end }} + {{- if eq .Values.data.antiAffinity "hard" }} + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchLabels: + app: "{{ template "elasticsearch.name" . }}" + release: "{{ .Release.Name }}" + component: "{{ .Values.data.name }}" + {{- else if eq .Values.data.antiAffinity "soft" }} + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app: "{{ template "elasticsearch.name" . }}" + release: "{{ .Release.Name }}" + component: "{{ .Values.data.name }}" + {{- end }} + {{- with .Values.data.nodeAffinity }} + nodeAffinity: +{{ toYaml . | indent 10 }} + {{- end }} +{{- if .Values.data.nodeSelector }} + nodeSelector: +{{ toYaml .Values.data.nodeSelector | indent 8 }} +{{- end }} +{{- if .Values.data.tolerations }} + tolerations: +{{ toYaml .Values.data.tolerations | indent 8 }} +{{- end }} +{{- if or .Values.extraInitContainers .Values.sysctlInitContainer.enabled .Values.chownInitContainer.enabled .Values.cluster.plugins }} + initContainers: +{{- end }} +{{- if .Values.sysctlInitContainer.enabled }} + # see https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html + # and https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration-memory.html#mlockall + - name: "sysctl" + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: {{ .Values.initImage.pullPolicy | quote }} + resources: +{{ toYaml .Values.data.initResources | indent 12 }} + command: ["sysctl", "-w", "vm.max_map_count=262144"] + securityContext: + privileged: true +{{- end }} +{{- if .Values.chownInitContainer.enabled }} + - name: "chown" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + resources: +{{ toYaml .Values.data.initResources | indent 12 }} + command: + - /bin/bash + - -c + - > + set -e; + set -x; + chown elasticsearch:elasticsearch /usr/share/elasticsearch/data; + for datadir in $(find /usr/share/elasticsearch/data -mindepth 1 -maxdepth 1 -not -name ".snapshot"); do + chown -R elasticsearch:elasticsearch $datadir; + done; + chown elasticsearch:elasticsearch /usr/share/elasticsearch/logs; + for logfile in $(find /usr/share/elasticsearch/logs -mindepth 1 -maxdepth 1 -not -name ".snapshot"); do + chown -R elasticsearch:elasticsearch $logfile; + done + securityContext: + runAsUser: 0 + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: data +{{- end }} +{{- if .Values.extraInitContainers }} +{{ tpl .Values.extraInitContainers . | indent 6 }} +{{- end }} +{{- if .Values.cluster.plugins }} +{{ include "plugin-installer" . | indent 6 }} +{{- end }} + containers: + - name: elasticsearch + env: + - name: DISCOVERY_SERVICE + value: {{ template "elasticsearch.fullname" . }}-discovery + - name: NODE_MASTER + value: "false" + - name: PROCESSORS + valueFrom: + resourceFieldRef: + resource: limits.cpu + - name: ES_JAVA_OPTS + value: "-Djava.net.preferIPv4Stack=true -Xms{{ .Values.data.heapSize }} -Xmx{{ .Values.data.heapSize }} {{ .Values.cluster.additionalJavaOpts }} {{ .Values.data.additionalJavaOpts }}" + {{- range $key, $value := .Values.cluster.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + {{- end }} + ports: + - containerPort: 9300 + name: transport +{{ if .Values.data.exposeHttp }} + - containerPort: 9200 + name: http +{{ end }} + resources: +{{ toYaml .Values.data.resources | indent 12 }} + readinessProbe: +{{ toYaml .Values.data.readinessProbe | indent 10 }} + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: data + - mountPath: /usr/share/elasticsearch/config/elasticsearch.yml + name: config + subPath: elasticsearch.yml +{{- if .Values.cluster.plugins }} + - mountPath: /usr/share/elasticsearch/plugins/ + name: plugindir +{{- end }} +{{- if hasPrefix "2." .Values.appVersion }} + - mountPath: /usr/share/elasticsearch/config/logging.yml + name: config + subPath: logging.yml +{{- end }} +{{- if hasPrefix "5." .Values.appVersion }} + - mountPath: /usr/share/elasticsearch/config/log4j2.properties + name: config + subPath: log4j2.properties +{{- end }} +{{- if .Values.cluster.keystoreSecret }} + - name: keystore + mountPath: "/usr/share/elasticsearch/config/elasticsearch.keystore" + subPath: elasticsearch.keystore + readOnly: true +{{- end }} +{{- if or .Values.data.hooks.preStop .Values.data.hooks.drain.enabled }} + - name: config + mountPath: /data-pre-stop-hook.sh + subPath: data-pre-stop-hook.sh +{{- end }} +{{- if or .Values.data.hooks.postStart .Values.data.hooks.drain.enabled }} + - name: config + mountPath: /data-post-start-hook.sh + subPath: data-post-start-hook.sh +{{- end }} +{{- if or .Values.data.hooks.preStop .Values.data.hooks.postStart .Values.data.hooks.drain.enabled }} + lifecycle: + {{- if or .Values.data.hooks.preStop .Values.data.hooks.drain.enabled }} + preStop: + exec: + command: ["/bin/bash","/data-pre-stop-hook.sh"] + {{- end }} + {{- if or .Values.data.hooks.postStart .Values.data.hooks.drain.enabled }} + postStart: + exec: + command: ["/bin/bash","/data-post-start-hook.sh"] + {{- end }} +{{- end }} + terminationGracePeriodSeconds: {{ .Values.data.terminationGracePeriodSeconds }} +{{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range $pullSecret := .Values.image.pullSecrets }} + - name: {{ $pullSecret }} + {{- end }} +{{- end }} + volumes: + - name: config + configMap: + name: {{ template "elasticsearch.fullname" . }} +{{- if .Values.cluster.plugins }} + - name: plugindir + emptyDir: {} +{{- end }} +{{- if .Values.cluster.keystoreSecret }} + - name: keystore + secret: + secretName: {{ .Values.cluster.keystoreSecret }} +{{- end }} + {{- if not .Values.data.persistence.enabled }} + - name: data + emptyDir: {} + {{- end }} + podManagementPolicy: {{ .Values.data.podManagementPolicy }} + updateStrategy: + type: {{ .Values.data.updateStrategy.type }} + {{- if .Values.data.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: {{ .Values.data.persistence.name }} + spec: + accessModes: + - {{ .Values.data.persistence.accessMode | quote }} + {{- if .Values.data.persistence.storageClass }} + {{- if (eq "-" .Values.data.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.data.persistence.storageClass }}" + {{- end }} + {{- end }} + resources: + requests: + storage: "{{ .Values.data.persistence.size }}" + {{- end }} diff --git a/deploy/helm/elasticsearch/templates/job.yaml b/deploy/helm/elasticsearch/templates/job.yaml new file mode 100755 index 00000000..f3754975 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/job.yaml @@ -0,0 +1,34 @@ +{{- if .Values.cluster.bootstrapShellCommand }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ template "elasticsearch.fullname" . }}-bootstrap + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "10" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + metadata: + name: {{ template "elasticsearch.fullname" . }}-bootstrap + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + spec: + containers: + - name: bootstrap-elasticsearch + image: byrnedo/alpine-curl + command: + - "sh" + - "-c" + - {{ .Values.cluster.bootstrapShellCommand | quote }} + restartPolicy: Never + backoffLimit: 20 +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/master-pdb.yaml b/deploy/helm/elasticsearch/templates/master-pdb.yaml new file mode 100755 index 00000000..c3efe835 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/master-pdb.yaml @@ -0,0 +1,24 @@ +{{- if .Values.master.podDisruptionBudget.enabled }} +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.master.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.master.fullname" . }} +spec: +{{- if .Values.master.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.master.podDisruptionBudget.minAvailable }} +{{- end }} +{{- if .Values.master.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.master.podDisruptionBudget.maxUnavailable }} +{{- end }} + selector: + matchLabels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.master.name }}" + release: {{ .Release.Name }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/master-serviceaccount.yaml b/deploy/helm/elasticsearch/templates/master-serviceaccount.yaml new file mode 100755 index 00000000..0f7dfbdb --- /dev/null +++ b/deploy/helm/elasticsearch/templates/master-serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccounts.master.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.master.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.master.fullname" . }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/master-statefulset.yaml b/deploy/helm/elasticsearch/templates/master-statefulset.yaml new file mode 100755 index 00000000..b7fc7b6c --- /dev/null +++ b/deploy/helm/elasticsearch/templates/master-statefulset.yaml @@ -0,0 +1,262 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.master.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.master.fullname" . }} +spec: + selector: + matchLabels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.master.name }}" + release: {{ .Release.Name }} + role: master + serviceName: {{ template "elasticsearch.master.fullname" . }} + replicas: {{ .Values.master.replicas }} + template: + metadata: + labels: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.master.name }}" + release: {{ .Release.Name }} + role: master +{{- if or .Values.master.podAnnotations (eq .Values.master.updateStrategy.type "RollingUpdate") }} + annotations: + {{- if .Values.master.podAnnotations }} +{{ toYaml .Values.master.podAnnotations | indent 8 }} + {{- end }} + {{- if eq .Values.master.updateStrategy.type "RollingUpdate" }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- end }} +{{- end }} + spec: + {{- if .Values.schedulerName }} + schedulerName: "{{ .Values.schedulerName }}" + {{- end }} + serviceAccountName: {{ template "elasticsearch.serviceAccountName.master" . }} +{{- if .Values.master.priorityClassName }} + priorityClassName: "{{ .Values.master.priorityClassName }}" +{{- end }} + securityContext: + fsGroup: 1000 + {{- if or .Values.master.antiAffinity .Values.master.nodeAffinity }} + affinity: + {{- end }} + {{- if eq .Values.master.antiAffinity "hard" }} + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchLabels: + app: "{{ template "elasticsearch.name" . }}" + release: "{{ .Release.Name }}" + component: "{{ .Values.master.name }}" + {{- else if eq .Values.master.antiAffinity "soft" }} + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app: "{{ template "elasticsearch.name" . }}" + release: "{{ .Release.Name }}" + component: "{{ .Values.master.name }}" + {{- end }} + {{- with .Values.master.nodeAffinity }} + nodeAffinity: +{{ toYaml . | indent 10 }} + {{- end }} +{{- if .Values.master.nodeSelector }} + nodeSelector: +{{ toYaml .Values.master.nodeSelector | indent 8 }} +{{- end }} +{{- if .Values.master.tolerations }} + tolerations: +{{ toYaml .Values.master.tolerations | indent 8 }} +{{- end }} +{{- if .Values.master.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ .Values.master.terminationGracePeriodSeconds }} +{{- end }} +{{- if or .Values.extraInitContainers .Values.sysctlInitContainer.enabled .Values.chownInitContainer.enabled .Values.cluster.plugins }} + initContainers: +{{- end }} +{{- if .Values.sysctlInitContainer.enabled }} + # see https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html + # and https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration-memory.html#mlockall + - name: "sysctl" + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: {{ .Values.initImage.pullPolicy | quote }} + resources: +{{ toYaml .Values.master.initResources | indent 12 }} + command: ["sysctl", "-w", "vm.max_map_count=262144"] + securityContext: + privileged: true +{{- end }} +{{- if .Values.chownInitContainer.enabled }} + - name: "chown" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + resources: +{{ toYaml .Values.master.initResources | indent 12 }} + command: + - /bin/bash + - -c + - > + set -e; + set -x; + chown elasticsearch:elasticsearch /usr/share/elasticsearch/data; + for datadir in $(find /usr/share/elasticsearch/data -mindepth 1 -maxdepth 1 -not -name ".snapshot"); do + chown -R elasticsearch:elasticsearch $datadir; + done; + chown elasticsearch:elasticsearch /usr/share/elasticsearch/logs; + for logfile in $(find /usr/share/elasticsearch/logs -mindepth 1 -maxdepth 1 -not -name ".snapshot"); do + chown -R elasticsearch:elasticsearch $logfile; + done + securityContext: + runAsUser: 0 + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: data +{{- end }} +{{- if .Values.extraInitContainers }} +{{ tpl .Values.extraInitContainers . | indent 6 }} +{{- end }} +{{- if .Values.cluster.plugins }} +{{ include "plugin-installer" . | indent 6 }} +{{- end }} + containers: + - name: elasticsearch + env: + - name: NODE_DATA + value: "false" +{{- if hasPrefix "5." .Values.appVersion }} + - name: NODE_INGEST + value: "false" +{{- end }} + - name: DISCOVERY_SERVICE + value: {{ template "elasticsearch.fullname" . }}-discovery + - name: PROCESSORS + valueFrom: + resourceFieldRef: + resource: limits.cpu + - name: ES_JAVA_OPTS + value: "-Djava.net.preferIPv4Stack=true -Xms{{ .Values.master.heapSize }} -Xmx{{ .Values.master.heapSize }} {{ .Values.cluster.additionalJavaOpts }} {{ .Values.master.additionalJavaOpts }}" + {{- range $key, $value := .Values.cluster.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + resources: +{{ toYaml .Values.master.resources | indent 12 }} + readinessProbe: +{{ toYaml .Values.master.readinessProbe | indent 10 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + {{- end }} + ports: + - containerPort: 9300 + name: transport +{{ if .Values.master.exposeHttp }} + - containerPort: 9200 + name: http +{{ end }} + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: data + - mountPath: /usr/share/elasticsearch/config/elasticsearch.yml + name: config + subPath: elasticsearch.yml +{{- if .Values.cluster.plugins }} + - mountPath: /usr/share/elasticsearch/plugins/ + name: plugindir +{{- end }} +{{- if hasPrefix "2." .Values.appVersion }} + - mountPath: /usr/share/elasticsearch/config/logging.yml + name: config + subPath: logging.yml +{{- end }} +{{- if hasPrefix "5." .Values.appVersion }} + - mountPath: /usr/share/elasticsearch/config/log4j2.properties + name: config + subPath: log4j2.properties +{{- end }} +{{- if .Values.cluster.keystoreSecret }} + - name: keystore + mountPath: "/usr/share/elasticsearch/config/elasticsearch.keystore" + subPath: elasticsearch.keystore + readOnly: true +{{- end }} +{{- if .Values.master.hooks.preStop }} + - name: config + mountPath: /master-pre-stop-hook.sh + subPath: master-pre-stop-hook.sh +{{- end }} +{{- if .Values.master.hooks.postStart }} + - name: config + mountPath: /master-post-start-hook.sh + subPath: master-post-start-hook.sh +{{- end }} +{{- if or .Values.master.hooks.preStop .Values.master.hooks.postStart }} + lifecycle: + {{- if .Values.master.hooks.preStop }} + preStop: + exec: + command: ["/bin/bash","/master-pre-stop-hook.sh"] + {{- end }} + {{- if .Values.master.hooks.postStart }} + postStart: + exec: + command: ["/bin/bash","/master-post-start-hook.sh"] + {{- end }} +{{- end }} +{{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range $pullSecret := .Values.image.pullSecrets }} + - name: {{ $pullSecret }} + {{- end }} +{{- end }} + volumes: + - name: config + configMap: + name: {{ template "elasticsearch.fullname" . }} +{{- if .Values.cluster.plugins }} + - name: plugindir + emptyDir: {} +{{- end }} +{{- if .Values.cluster.keystoreSecret }} + - name: keystore + secret: + secretName: {{ .Values.cluster.keystoreSecret }} +{{- end }} + {{- if not .Values.master.persistence.enabled }} + - name: data + emptyDir: {} + {{- end }} + podManagementPolicy: {{ .Values.master.podManagementPolicy }} + updateStrategy: + type: {{ .Values.master.updateStrategy.type }} + {{- if .Values.master.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: {{ .Values.master.persistence.name }} + spec: + accessModes: + - {{ .Values.master.persistence.accessMode | quote }} + {{- if .Values.master.persistence.storageClass }} + {{- if (eq "-" .Values.master.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.master.persistence.storageClass }}" + {{- end }} + {{- end }} + resources: + requests: + storage: "{{ .Values.master.persistence.size }}" + {{ end }} diff --git a/deploy/helm/elasticsearch/templates/master-svc.yaml b/deploy/helm/elasticsearch/templates/master-svc.yaml new file mode 100755 index 00000000..5db28b7f --- /dev/null +++ b/deploy/helm/elasticsearch/templates/master-svc.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.master.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "elasticsearch.fullname" . }}-discovery +spec: + clusterIP: None + ports: + - port: 9300 + targetPort: transport + selector: + app: {{ template "elasticsearch.name" . }} + component: "{{ .Values.master.name }}" + release: {{ .Release.Name }} diff --git a/deploy/helm/elasticsearch/templates/podsecuritypolicy.yaml b/deploy/helm/elasticsearch/templates/podsecuritypolicy.yaml new file mode 100755 index 00000000..fd5f663d --- /dev/null +++ b/deploy/helm/elasticsearch/templates/podsecuritypolicy.yaml @@ -0,0 +1,43 @@ +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ template "elasticsearch.fullname" . }} + labels: + app: {{ template "elasticsearch.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + annotations: +{{- if .Values.podSecurityPolicy.annotations }} +{{ toYaml .Values.podSecurityPolicy.annotations | indent 4 }} +{{- end }} +spec: + privileged: true + allowPrivilegeEscalation: true + volumes: + - 'configMap' + - 'secret' + - 'emptyDir' + - 'persistentVolumeClaim' + hostNetwork: false + hostPID: false + hostIPC: false + runAsUser: + rule: 'RunAsAny' + runAsGroup: + rule: 'RunAsAny' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'RunAsAny' + fsGroup: + rule: 'MustRunAs' + ranges: + - min: 1000 + max: 1000 + readOnlyRootFilesystem: false + hostPorts: + - min: 1 + max: 65535 +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/role.yaml b/deploy/helm/elasticsearch/templates/role.yaml new file mode 100755 index 00000000..1e329c57 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/role.yaml @@ -0,0 +1,17 @@ +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +metadata: + name: {{ template "elasticsearch.fullname" . }} + labels: + app: {{ template "elasticsearch.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +rules: +- apiGroups: ['extensions'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - {{ template "elasticsearch.fullname" . }} +{{- end }} diff --git a/deploy/helm/elasticsearch/templates/rolebinding.yaml b/deploy/helm/elasticsearch/templates/rolebinding.yaml new file mode 100755 index 00000000..3606960b --- /dev/null +++ b/deploy/helm/elasticsearch/templates/rolebinding.yaml @@ -0,0 +1,26 @@ +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: {{ template "elasticsearch.fullname" . }} + labels: + app: {{ template "elasticsearch.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +roleRef: + kind: Role + name: {{ template "elasticsearch.fullname" . }} + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: {{ template "elasticsearch.serviceAccountName.client" . }} + namespace: {{ .Release.Namespace }} +- kind: ServiceAccount + name: {{ template "elasticsearch.serviceAccountName.data" . }} + namespace: {{ .Release.Namespace }} +- kind: ServiceAccount + name: {{ template "elasticsearch.serviceAccountName.master" . }} + namespace: {{ .Release.Namespace }} +{{- end }} + diff --git a/deploy/helm/elasticsearch/templates/tests/test-configmap.yaml b/deploy/helm/elasticsearch/templates/tests/test-configmap.yaml new file mode 100755 index 00000000..f9a30c19 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/tests/test-configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "elasticsearch.fullname" . }}-test + labels: + app: {{ template "elasticsearch.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: "{{ .Release.Service }}" + release: "{{ .Release.Name }}" +data: + run.sh: |- + @test "Test Access and Health" { + curl -D - http://{{ template "elasticsearch.client.fullname" . }}:9200 + curl -D - http://{{ template "elasticsearch.client.fullname" . }}:9200/_cluster/health?wait_for_status=green + } diff --git a/deploy/helm/elasticsearch/templates/tests/test.yaml b/deploy/helm/elasticsearch/templates/tests/test.yaml new file mode 100755 index 00000000..56f11ac3 --- /dev/null +++ b/deploy/helm/elasticsearch/templates/tests/test.yaml @@ -0,0 +1,48 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "elasticsearch.fullname" . }}-test + labels: + app: {{ template "elasticsearch.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: "{{ .Release.Service }}" + release: "{{ .Release.Name }}" + annotations: + "helm.sh/hook": test-success +spec: +{{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range $pullSecret := .Values.image.pullSecrets }} + - name: {{ $pullSecret }} + {{- end }} +{{- end }} + initContainers: + - name: test-framework + image: "{{ .Values.testFramework.image}}:{{ .Values.testFramework.tag }}" + command: + - "bash" + - "-c" + - | + set -ex + # copy bats to tools dir + cp -R /usr/local/libexec/ /tools/bats/ + volumeMounts: + - mountPath: /tools + name: tools + containers: + - name: {{ .Release.Name }}-test + image: "{{ .Values.testFramework.image}}:{{ .Values.testFramework.tag }}" + command: ["/tools/bats/bats", "-t", "/tests/run.sh"] + volumeMounts: + - mountPath: /tests + name: tests + readOnly: true + - mountPath: /tools + name: tools + volumes: + - name: tests + configMap: + name: {{ template "elasticsearch.fullname" . }}-test + - name: tools + emptyDir: {} + restartPolicy: Never diff --git a/deploy/helm/elasticsearch/values.yaml b/deploy/helm/elasticsearch/values.yaml new file mode 100755 index 00000000..7a0e274c --- /dev/null +++ b/deploy/helm/elasticsearch/values.yaml @@ -0,0 +1,317 @@ +# Default values for elasticsearch. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +appVersion: "6.8.6" + +## Define serviceAccount names for components. Defaults to component's fully qualified name. +## +serviceAccounts: + client: + create: true + name: + master: + create: true + name: + data: + create: true + name: + +## Specify if a Pod Security Policy for node-exporter must be created +## Ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/ +## +podSecurityPolicy: + enabled: false + annotations: {} + ## Specify pod annotations + ## Ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#apparmor + ## Ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp + ## Ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#sysctl + ## + # seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' + # seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default' + # apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default' + +securityContext: + enabled: false + runAsUser: 1000 + +## Use an alternate scheduler, e.g. "stork". +## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/ +## +# schedulerName: "default-scheduler" + +image: + repository: "docker.elastic.co/elasticsearch/elasticsearch-oss" + tag: "6.8.6" + pullPolicy: "IfNotPresent" + # If specified, use these secrets to access the image + # pullSecrets: + # - registry-secret + +testFramework: + image: "dduportal/bats" + tag: "0.4.0" + +initImage: + repository: "busybox" + tag: "latest" + pullPolicy: "Always" + +cluster: + name: "elasticsearch" + # If you want X-Pack installed, switch to an image that includes it, enable this option and toggle the features you want + # enabled in the environment variables outlined in the README + xpackEnable: false + # Some settings must be placed in a keystore, so they need to be mounted in from a secret. + # Use this setting to specify the name of the secret + # keystoreSecret: eskeystore + config: {} + # Custom parameters, as string, to be added to ES_JAVA_OPTS environment variable + additionalJavaOpts: "" + # Command to run at the end of deployment + bootstrapShellCommand: "" + env: + # IMPORTANT: https://www.elastic.co/guide/en/elasticsearch/reference/current/important-settings.html#minimum_master_nodes + # To prevent data loss, it is vital to configure the discovery.zen.minimum_master_nodes setting so that each master-eligible + # node knows the minimum number of master-eligible nodes that must be visible in order to form a cluster. + MINIMUM_MASTER_NODES: "2" + # List of plugins to install via dedicated init container + plugins: [] + # - ingest-attachment + # - mapper-size + + loggingYml: + # you can override this using by setting a system property, for example -Des.logger.level=DEBUG + es.logger.level: INFO + rootLogger: ${es.logger.level}, console + logger: + # log action execution errors for easier debugging + action: DEBUG + # reduce the logging for aws, too much is logged under the default INFO + com.amazonaws: WARN + appender: + console: + type: console + layout: + type: consolePattern + conversionPattern: "[%d{ISO8601}][%-5p][%-25c] %m%n" + + log4j2Properties: | + status = error + appender.console.type = Console + appender.console.name = console + appender.console.layout.type = PatternLayout + appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n + rootLogger.level = info + rootLogger.appenderRef.console.ref = console + logger.searchguard.name = com.floragunn + logger.searchguard.level = info + +client: + name: client + replicas: 2 + serviceType: ClusterIP + ## If coupled with serviceType = "NodePort", this will set a specific nodePort to the client HTTP port + # httpNodePort: 30920 + loadBalancerIP: {} + loadBalancerSourceRanges: {} +## (dict) If specified, apply these annotations to the client service +# serviceAnnotations: +# example: client-svc-foo + heapSize: "512m" + # additionalJavaOpts: "-XX:MaxRAM=512m" + antiAffinity: "soft" + nodeAffinity: {} + nodeSelector: {} + tolerations: [] + # terminationGracePeriodSeconds: 60 + initResources: {} + # limits: + # cpu: "25m" + # # memory: "128Mi" + # requests: + # cpu: "25m" + # memory: "128Mi" + resources: + limits: + cpu: "1" + # memory: "1024Mi" + requests: + cpu: "25m" + memory: "512Mi" + priorityClassName: "" + ## (dict) If specified, apply these annotations to each client Pod + # podAnnotations: + # example: client-foo + podDisruptionBudget: + enabled: false + minAvailable: 1 + # maxUnavailable: 1 + hooks: {} + ## (string) Script to execute prior the client pod stops. + # preStop: |- + + ## (string) Script to execute after the client pod starts. + # postStart: |- + ingress: + enabled: false + # user: NAME + # password: PASSWORD + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +master: + name: master + exposeHttp: false + replicas: 3 + heapSize: "512m" + # additionalJavaOpts: "-XX:MaxRAM=512m" + persistence: + enabled: true + accessMode: ReadWriteOnce + name: data + size: "4Gi" + # storageClass: "ssd" + readinessProbe: + httpGet: + path: /_cluster/health?local=true + port: 9200 + initialDelaySeconds: 5 + antiAffinity: "soft" + nodeAffinity: {} + nodeSelector: {} + tolerations: [] + # terminationGracePeriodSeconds: 60 + initResources: {} + # limits: + # cpu: "25m" + # # memory: "128Mi" + # requests: + # cpu: "25m" + # memory: "128Mi" + resources: + limits: + cpu: "1" + # memory: "1024Mi" + requests: + cpu: "25m" + memory: "512Mi" + priorityClassName: "" + ## (dict) If specified, apply these annotations to each master Pod + # podAnnotations: + # example: master-foo + podManagementPolicy: OrderedReady + podDisruptionBudget: + enabled: false + minAvailable: 2 # Same as `cluster.env.MINIMUM_MASTER_NODES` + # maxUnavailable: 1 + updateStrategy: + type: OnDelete + hooks: {} + ## (string) Script to execute prior the master pod stops. + # preStop: |- + + ## (string) Script to execute after the master pod starts. + # postStart: |- + +data: + name: data + exposeHttp: false + replicas: 2 + heapSize: "1536m" + # additionalJavaOpts: "-XX:MaxRAM=1536m" + persistence: + enabled: true + accessMode: ReadWriteOnce + name: data + size: "30Gi" + # storageClass: "ssd" + readinessProbe: + httpGet: + path: /_cluster/health?local=true + port: 9200 + initialDelaySeconds: 5 + terminationGracePeriodSeconds: 3600 + antiAffinity: "soft" + nodeAffinity: {} + nodeSelector: {} + tolerations: [] + initResources: {} + # limits: + # cpu: "25m" + # # memory: "128Mi" + # requests: + # cpu: "25m" + # memory: "128Mi" + resources: + limits: + cpu: "1" + # memory: "2048Mi" + requests: + cpu: "25m" + memory: "1536Mi" + priorityClassName: "" + ## (dict) If specified, apply these annotations to each data Pod + # podAnnotations: + # example: data-foo + podDisruptionBudget: + enabled: false + # minAvailable: 1 + maxUnavailable: 1 + podManagementPolicy: OrderedReady + updateStrategy: + type: OnDelete + hooks: + ## Drain the node before stopping it and re-integrate it into the cluster after start. + ## When enabled, it supersedes `data.hooks.preStop` and `data.hooks.postStart` defined below. + drain: + enabled: true + + ## (string) Script to execute prior the data pod stops. Ignored if `data.hooks.drain.enabled` is true (default) + # preStop: |- + # #!/bin/bash + # exec &> >(tee -a "/var/log/elasticsearch-hooks.log") + # NODE_NAME=${HOSTNAME} + # curl -s -XPUT -H 'Content-Type: application/json' '{{ template "elasticsearch.client.fullname" . }}:9200/_cluster/settings' -d "{ + # \"transient\" :{ + # \"cluster.routing.allocation.exclude._name\" : \"${NODE_NAME}\" + # } + # }" + # echo "Node ${NODE_NAME} is exluded from the allocation" + + ## (string) Script to execute after the data pod starts. Ignored if `data.hooks.drain.enabled` is true (default) + # postStart: |- + # #!/bin/bash + # exec &> >(tee -a "/var/log/elasticsearch-hooks.log") + # NODE_NAME=${HOSTNAME} + # CLUSTER_SETTINGS=$(curl -s -XGET "http://{{ template "elasticsearch.client.fullname" . }}:9200/_cluster/settings") + # if echo "${CLUSTER_SETTINGS}" | grep -E "${NODE_NAME}"; then + # echo "Activate node ${NODE_NAME}" + # curl -s -XPUT -H 'Content-Type: application/json' "http://{{ template "elasticsearch.client.fullname" . }}:9200/_cluster/settings" -d "{ + # \"transient\" :{ + # \"cluster.routing.allocation.exclude._name\" : null + # } + # }" + # fi + # echo "Node ${NODE_NAME} is ready to be used" + +## Sysctl init container to setup vm.max_map_count +# see https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html +# and https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration-memory.html#mlockall +sysctlInitContainer: + enabled: true +## Chown init container to change ownership of data and logs directories to elasticsearch user +chownInitContainer: + enabled: true +## Additional init containers +extraInitContainers: | + +forceIpv6: false diff --git a/deploy/helm/kibana-3.2.8.tgz b/deploy/helm/kibana-3.2.8.tgz new file mode 100644 index 00000000..2475a6cf Binary files /dev/null and b/deploy/helm/kibana-3.2.8.tgz differ diff --git a/deploy/helm/kibana/.helmignore b/deploy/helm/kibana/.helmignore new file mode 100755 index 00000000..c13e3c8f --- /dev/null +++ b/deploy/helm/kibana/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj \ No newline at end of file diff --git a/deploy/helm/kibana/Chart.yaml b/deploy/helm/kibana/Chart.yaml new file mode 100755 index 00000000..801603f1 --- /dev/null +++ b/deploy/helm/kibana/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +appVersion: 6.7.0 +deprecated: true +description: DEPRECATED - Kibana is an open source data visualization plugin for Elasticsearch +engine: gotpl +home: https://www.elastic.co/products/kibana +icon: https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8781708f8f37ed16/5c11ec2edf09df047814db23/logo-elastic-kibana-lt.svg +keywords: +- elasticsearch +- kibana +name: kibana +sources: +- https://github.com/elastic/kibana +version: 3.2.8 diff --git a/deploy/helm/kibana/OWNERS b/deploy/helm/kibana/OWNERS new file mode 100755 index 00000000..71578f02 --- /dev/null +++ b/deploy/helm/kibana/OWNERS @@ -0,0 +1,6 @@ +approvers: +- compleatang +- monotek +reviewers: +- compleatang +- monotek diff --git a/deploy/helm/kibana/README.md b/deploy/helm/kibana/README.md new file mode 100755 index 00000000..c6ca4256 --- /dev/null +++ b/deploy/helm/kibana/README.md @@ -0,0 +1,156 @@ +# kibana + +**CHART WAS DEPRECATED! USE THE OFFICIAL CHART INSTEAD @ ** + +[kibana](https://github.com/elastic/kibana) is your window into the Elastic Stack. Specifically, it's an open source (Apache Licensed), browser-based analytics and search dashboard for Elasticsearch. + +## Pre-deprecation notice +As mentioned in #14935 we are planning on deprecating this chart in favour of the official Elastic Helm Chart. The Elastic Helm Chart supports version 7 of Kibana. + +## TL;DR; + +```console +$ helm install stable/kibana +``` + +## Introduction + +This chart bootstraps a kibana deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```console +$ helm install stable/kibana --name my-release +``` + +The command deploys kibana on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. + +NOTE : We notice that lower resource constraints given to the chart + plugins are likely not going to work well. + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```console +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Configuration + +The following table lists the configurable parameters of the kibana chart and their default values. + +| Parameter | Description | Default | +| ------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------- | +| `affinity` | node/pod affinities | None | +| `env` | Environment variables to configure Kibana | `{}` | +| `envFromSecrets` | Environment variables from secrets to the cronjob container | {} | +| `envFromSecrets.*.from.secret` | - `secretKeyRef.name` used for environment variable | | +| `envFromSecrets.*.from.key` | - `secretKeyRef.key` used for environment variable | | +| `files` | Kibana configuration files | None | +| `livenessProbe.enabled` | livenessProbe to be enabled? | `false` | +| `livenessProbe.path` | path for livenessProbe | `/status` | +| `livenessProbe.initialDelaySeconds` | number of seconds | 30 | +| `livenessProbe.timeoutSeconds` | number of seconds | 10 | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.repository` | Image repository | `docker.elastic.co/kibana/kibana-oss` | +| `image.tag` | Image tag | `6.7.0` | +| `image.pullSecrets` | Specify image pull secrets | `nil` | +| `commandline.args` | add additional commandline args | `nil` | +| `ingress.enabled` | Enables Ingress | `false` | +| `ingress.annotations` | Ingress annotations | None: | +| `ingress.hosts` | Ingress accepted hostnames | None: | +| `ingress.tls` | Ingress TLS configuration | None: | +| `nodeSelector` | node labels for pod assignment | `{}` | +| `podAnnotations` | annotations to add to each pod | `{}` | +| `podLabels` | labels to add to each pod | `{}` | +| `replicaCount` | desired number of pods | `1` | +| `revisionHistoryLimit` | revisionHistoryLimit | `3` | +| `serviceAccountName` | DEPRECATED: use serviceAccount.name | `nil` | +| `serviceAccount.create` | create a serviceAccount to run the pod | `false` | +| `serviceAccount.name` | name of the serviceAccount to create | `kibana.fullname` | +| `authProxyEnabled` | enables authproxy. Create container in extracontainers | `false` | +| `extraContainers` | Sidecar containers to add to the kibana pod | `{}` | +| `extraVolumeMounts` | additional volumemounts for the kibana pod | `[]` | +| `extraVolumes` | additional volumes to add to the kibana pod | `[]` | +| `resources` | pod resource requests & limits | `{}` | +| `priorityClassName` | priorityClassName | `nil` | +| `service.externalPort` | external port for the service | `443` | +| `service.internalPort` | internal port for the service | `4180` | +| `service.portName` | service port name | None: | +| `service.authProxyPort` | port to use when using sidecar authProxy | None: | +| `service.externalIPs` | external IP addresses | None: | +| `service.loadBalancerIP` | Load Balancer IP address | None: | +| `service.loadBalancerSourceRanges` | Limit load balancer source IPs to list of CIDRs (where available)) | `[]` | +| `service.nodePort` | NodePort value if service.type is NodePort | None: | +| `service.type` | type of service | `ClusterIP` | +| `service.clusterIP` | static clusterIP or None for headless services | None: | +| `service.annotations` | Kubernetes service annotations | None: | +| `service.labels` | Kubernetes service labels | None: | +| `service.selector` | Kubernetes service selector | `{}` | +| `tolerations` | List of node taints to tolerate | `[]` | +| `dashboardImport.enabled` | Enable dashboard import | `false` | +| `dashboardImport.timeout` | Time in seconds waiting for Kibana to be in green overall state | `60` | +| `dashboardImport.basePath` | Customizing base path url during dashboard import | `/` | +| `dashboardImport.xpackauth.enabled` | Enable Xpack auth | `false` | +| `dashboardImport.xpackauth.username` | Optional Xpack username | `myuser` | +| `dashboardImport.xpackauth.password` | Optional Xpack password | `mypass` | +| `dashboardImport.dashboards` | Dashboards | `{}` | +| `plugins.enabled` | Enable installation of plugins. | `false` | +| `plugins.reset` | Optional : Remove all installed plugins before installing all new ones | `false` | +| `plugins.values` | List of plugins to install. Format | None: | +| `persistentVolumeClaim.enabled` | Enable PVC for plugins | `false` | +| `persistentVolumeClaim.existingClaim` | Use your own PVC for plugins | `false` | +| `persistentVolumeClaim.annotations` | Add your annotations for the PVC | `{}` | +| `persistentVolumeClaim.accessModes` | Acces mode to the PVC | `ReadWriteOnce` | +| `persistentVolumeClaim.size` | Size of the PVC | `5Gi` | +| `persistentVolumeClaim.storageClass` | Storage class of the PVC | None: | +| `readinessProbe.enabled` | readinessProbe to be enabled? | `false` | +| `readinessProbe.path` | path for readinessProbe | `/status` | +| `readinessProbe.initialDelaySeconds` | number of seconds | 30 | +| `readinessProbe.timeoutSeconds` | number of seconds | 10 | +| `readinessProbe.periodSeconds` | number of seconds | 10 | +| `readinessProbe.successThreshold` | number of successes | 5 | +| `securityContext.enabled` | Enable security context (should be true for PVC) | `false` | +| `securityContext.allowPrivilegeEscalation` | Allow privilege escalation | `false` | +| `securityContext.runAsUser` | User id to run in pods | `1000` | +| `securityContext.fsGroup` | fsGroup id to run in pods | `2000` | +| `extraConfigMapMounts` | Additional configmaps to be mounted | `[]` | +| `deployment.annotations` | Annotations for deployment | `{}` | +| `initContainers` | Init containers to add to the kibana deployment | `{}` | +| `testFramework.enabled` | enable the test framework | true | +| `testFramework.image` | `test-framework` image repository. | `dduportal/bats` | +| `testFramework.tag` | `test-framework` image tag. | `0.4.0` | + + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +- The Kibana configuration files config properties can be set through the `env` parameter too. +- All the files listed under this variable will overwrite any existing files by the same name in kibana config directory. +- Files not mentioned under this variable will remain unaffected. + +```console +$ helm install stable/kibana --name my-release \ + --set=image.tag=v0.0.2,resources.limits.cpu=200m +``` + +Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example : + +```console +$ helm install stable/kibana --name my-release -f values.yaml +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + +## Dasboard import + +- A dashboard for dashboardImport.dashboards can be a JSON or a download url to a JSON file. + +## Upgrading + +### To 2.3.0 + +The default value of `elasticsearch.url` (for kibana < 6.6) has been removed in favor of `elasticsearch.hosts` (for kibana >= 6.6). diff --git a/deploy/helm/kibana/ci/authproxy-enabled.yaml b/deploy/helm/kibana/ci/authproxy-enabled.yaml new file mode 100755 index 00000000..186724a5 --- /dev/null +++ b/deploy/helm/kibana/ci/authproxy-enabled.yaml @@ -0,0 +1,3 @@ +--- +# disable internal port by setting authProxyEnabled +authProxyEnabled: true diff --git a/deploy/helm/kibana/ci/dashboard-values.yaml b/deploy/helm/kibana/ci/dashboard-values.yaml new file mode 100755 index 00000000..e3516cee --- /dev/null +++ b/deploy/helm/kibana/ci/dashboard-values.yaml @@ -0,0 +1,21 @@ +--- +# enable the dashboard init container with dashboard embedded in configmap + +dashboardImport: + enabled: true + dashboards: + 1_create_index: |- + { + "version": "6.7.0", + "objects": [ + { + "id": "a88738e0-d3c1-11e8-b38e-a37c21cf8c95", + "version": 2, + "attributes": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + } + } + ] + } diff --git a/deploy/helm/kibana/ci/extra-configmap-mounts.yaml b/deploy/helm/kibana/ci/extra-configmap-mounts.yaml new file mode 100755 index 00000000..c4ccd572 --- /dev/null +++ b/deploy/helm/kibana/ci/extra-configmap-mounts.yaml @@ -0,0 +1,6 @@ +--- +extraConfigMapMounts: + - name: logtrail-configs + configMap: kibana-logtrail + mountPath: /usr/share/kibana/plugins/logtrail/logtrail.json + subPath: logtrail.json diff --git a/deploy/helm/kibana/ci/ingress-hosts-paths.yaml b/deploy/helm/kibana/ci/ingress-hosts-paths.yaml new file mode 100755 index 00000000..f9cca09c --- /dev/null +++ b/deploy/helm/kibana/ci/ingress-hosts-paths.yaml @@ -0,0 +1,3 @@ +ingress: + hosts: + - localhost.localdomain/kibana diff --git a/deploy/helm/kibana/ci/ingress-hosts.yaml b/deploy/helm/kibana/ci/ingress-hosts.yaml new file mode 100755 index 00000000..7d8de7a9 --- /dev/null +++ b/deploy/helm/kibana/ci/ingress-hosts.yaml @@ -0,0 +1,3 @@ +ingress: + hosts: + - kibana.localhost.localdomain diff --git a/deploy/helm/kibana/ci/initcontainers-all-values.yaml b/deploy/helm/kibana/ci/initcontainers-all-values.yaml new file mode 100755 index 00000000..297220bd --- /dev/null +++ b/deploy/helm/kibana/ci/initcontainers-all-values.yaml @@ -0,0 +1,23 @@ +--- +# enable all init container types + +# A dashboard is defined by a name and a string with the json payload or the download url +dashboardImport: + enabled: true + dashboards: + k8s: https://raw.githubusercontent.com/monotek/kibana-dashboards/master/k8s-fluentd-elasticsearch.json + +# Enable the plugin init container with plugins retrieved from an URL +plugins: + enabled: true + reset: false + # Use to add/upgrade plugin + values: + - analyze-api-ui-plugin,6.7.0,https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.7.0/analyze-api-ui-plugin-6.7.0.zip + # - other_plugin + +# Add your own init container +initContainers: + echo-container: + image: "busybox" + command: ['sh', '-c', 'echo Hello from init container! && sleep 3'] diff --git a/deploy/helm/kibana/ci/initcontainers-values.yaml b/deploy/helm/kibana/ci/initcontainers-values.yaml new file mode 100755 index 00000000..70d939c6 --- /dev/null +++ b/deploy/helm/kibana/ci/initcontainers-values.yaml @@ -0,0 +1,18 @@ +--- +# enable user-defined init containers + +initContainers: + numbers-container: + image: "busybox" + imagePullPolicy: "IfNotPresent" + command: + - "/bin/sh" + - "-c" + - | + for i in $(seq 1 10); do + echo $i + done + + echo-container: + image: "busybox" + command: ['sh', '-c', 'echo Hello from init container! && sleep 3'] diff --git a/deploy/helm/kibana/ci/plugin-install.yaml b/deploy/helm/kibana/ci/plugin-install.yaml new file mode 100755 index 00000000..6c9da5cd --- /dev/null +++ b/deploy/helm/kibana/ci/plugin-install.yaml @@ -0,0 +1,9 @@ +--- +# enable the plugin init container with plugins retrieved from an URL +plugins: + enabled: true + reset: false + # Use to add/upgrade plugin + values: + - analyze-api-ui-plugin,6.7.0,https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.7.0/analyze-api-ui-plugin-6.7.0.zip + # - other_plugin diff --git a/deploy/helm/kibana/ci/pvc.yaml b/deploy/helm/kibana/ci/pvc.yaml new file mode 100755 index 00000000..3f96fa0e --- /dev/null +++ b/deploy/helm/kibana/ci/pvc.yaml @@ -0,0 +1,11 @@ +--- +persistentVolumeClaim: + # set to true to use pvc + enabled: true + # set to true to use you own pvc + existingClaim: false + annotations: {} + + accessModes: + - ReadWriteOnce + size: "5Gi" \ No newline at end of file diff --git a/deploy/helm/kibana/ci/security-context.yaml b/deploy/helm/kibana/ci/security-context.yaml new file mode 100755 index 00000000..d12ea2b5 --- /dev/null +++ b/deploy/helm/kibana/ci/security-context.yaml @@ -0,0 +1,6 @@ +--- +securityContext: + enabled: true + allowPrivilegeEscalation: false + runAsUser: 1000 + fsGroup: 2000 \ No newline at end of file diff --git a/deploy/helm/kibana/ci/service-values.yaml b/deploy/helm/kibana/ci/service-values.yaml new file mode 100755 index 00000000..cebf52ad --- /dev/null +++ b/deploy/helm/kibana/ci/service-values.yaml @@ -0,0 +1,4 @@ +--- +service: + selector: + foo: bar diff --git a/deploy/helm/kibana/ci/url_dashboard-values.yaml b/deploy/helm/kibana/ci/url_dashboard-values.yaml new file mode 100755 index 00000000..7149314d --- /dev/null +++ b/deploy/helm/kibana/ci/url_dashboard-values.yaml @@ -0,0 +1,7 @@ +--- +# enable the dashboard init container with dashboard retrieved from an URL + +dashboardImport: + enabled: true + dashboards: + k8s: https://raw.githubusercontent.com/monotek/kibana-dashboards/master/k8s-fluentd-elasticsearch.json diff --git a/deploy/helm/kibana/counter.yaml b/deploy/helm/kibana/counter.yaml new file mode 100644 index 00000000..cbcc8efc --- /dev/null +++ b/deploy/helm/kibana/counter.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: counter +spec: + containers: + - name: count + image: busybox + args: [/bin/sh, -c, 'i=0; while true; do echo "This is demo log $i: $(date)"; i=$((i+1)); sleep 1; done'] \ No newline at end of file diff --git a/deploy/helm/kibana/kibana-values.yaml b/deploy/helm/kibana/kibana-values.yaml new file mode 100644 index 00000000..13f68deb --- /dev/null +++ b/deploy/helm/kibana/kibana-values.yaml @@ -0,0 +1,10 @@ +files: + kibana.yml: + ## Default Kibana configuration from kibana-docker. + server.name: kibana + server.host: "0" + ## For kibana < 6.6, use elasticsearch.url instead + elasticsearch.hosts: http://elasticsearch-client:9200 + +service: + type: LoadBalancer # ClusterIP diff --git a/deploy/helm/kibana/templates/NOTES.txt b/deploy/helm/kibana/templates/NOTES.txt new file mode 100755 index 00000000..c489daf0 --- /dev/null +++ b/deploy/helm/kibana/templates/NOTES.txt @@ -0,0 +1,23 @@ + +THE CHART HAS BEEN DEPRECATED! + +Find the new official version @ https://github.com/elastic/helm-charts/tree/master/kibana + +To verify that {{ template "kibana.fullname" . }} has started, run: + + kubectl --namespace={{ .Release.Namespace }} get pods -l "app={{ template "kibana.name" . }}" + +Kibana can be accessed: + + * From outside the cluster, run these commands in the same shell: + {{- if contains "NodePort" .Values.service.type }} + + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "kibana.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + {{- else if contains "ClusterIP" .Values.service.type }} + + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "kibana.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:5601 to use Kibana" + kubectl port-forward --namespace {{ .Release.Namespace }} $POD_NAME 5601:5601 + {{- end }} diff --git a/deploy/helm/kibana/templates/_helpers.tpl b/deploy/helm/kibana/templates/_helpers.tpl new file mode 100755 index 00000000..c6c30e98 --- /dev/null +++ b/deploy/helm/kibana/templates/_helpers.tpl @@ -0,0 +1,40 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "kibana.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "kibana.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- printf .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "kibana.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{ default (include "kibana.fullname" .) .Values.serviceAccount.name }} +{{- else -}} +{{- if .Values.serviceAccountName -}} +{{- .Values.serviceAccountName }} +{{- else -}} +{{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/kibana/templates/configmap-dashboardimport.yaml b/deploy/helm/kibana/templates/configmap-dashboardimport.yaml new file mode 100755 index 00000000..f3741ff1 --- /dev/null +++ b/deploy/helm/kibana/templates/configmap-dashboardimport.yaml @@ -0,0 +1,67 @@ +{{- if .Values.dashboardImport.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "kibana.fullname" . }}-importscript + labels: + app: {{ template "kibana.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: + dashboardImport.sh: | + #!/usr/bin/env bash + # + # kibana dashboard import script + # + + cd /kibanadashboards + + echo "Starting Kibana..." + + /usr/local/bin/kibana-docker $@ & + + echo "Waiting up to {{ .Values.dashboardImport.timeout }} seconds for Kibana to get in green overall state..." + + for i in {1..{{ .Values.dashboardImport.timeout }}}; do + curl -s localhost:5601{{ .Values.dashboardImport.basePath }}/api/status | python -c 'import sys, json; print json.load(sys.stdin)["status"]["overall"]["state"]' 2> /dev/null | grep green > /dev/null && break || sleep 1 + done + + for DASHBOARD_FILE in *; do + echo -e "Importing ${DASHBOARD_FILE} dashboard..." + + if ! python -c 'import sys, json; print json.load(sys.stdin)' < "${DASHBOARD_FILE}" &> /dev/null ; then + echo "${DASHBOARD_FILE} is not valid JSON, assuming it's an URL..." + TMP_FILE="$(mktemp)" + curl -s $(cat ${DASHBOARD_FILE}) > ${TMP_FILE} + curl -v {{ if .Values.dashboardImport.xpackauth.enabled }}--user {{ .Values.dashboardImport.xpackauth.username }}:{{ .Values.dashboardImport.xpackauth.password }}{{ end }} -s --connect-timeout 60 --max-time 60 -XPOST localhost:5601{{ .Values.dashboardImport.basePath }}/api/kibana/dashboards/import?force=true -H 'kbn-xsrf:true' -H 'Content-type:application/json' -d @${TMP_FILE} + rm ${TMP_FILE} + else + echo "Valid JSON found in ${DASHBOARD_FILE}, importing..." + curl -v {{ if .Values.dashboardImport.xpackauth.enabled }}--user {{ .Values.dashboardImport.xpackauth.username }}:{{ .Values.dashboardImport.xpackauth.password }}{{ end }} -s --connect-timeout 60 --max-time 60 -XPOST localhost:5601{{ .Values.dashboardImport.basePath }}/api/kibana/dashboards/import?force=true -H 'kbn-xsrf:true' -H 'Content-type:application/json' -d @./${DASHBOARD_FILE} + fi + + if [ "$?" != "0" ]; then + echo -e "\nImport of ${DASHBOARD_FILE} dashboard failed... Exiting..." + exit 1 + else + echo -e "\nImport of ${DASHBOARD_FILE} dashboard finished :-)" + fi + + done +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "kibana.fullname" . }}-dashboards + labels: + app: {{ template "kibana.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: +{{- range $key, $value := .Values.dashboardImport.dashboards }} + {{ $key }}: |- +{{ $value | indent 4 }} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/kibana/templates/configmap.yaml b/deploy/helm/kibana/templates/configmap.yaml new file mode 100755 index 00000000..610da723 --- /dev/null +++ b/deploy/helm/kibana/templates/configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "kibana.fullname" . }} + labels: + app: {{ template "kibana.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: +{{- range $key, $value := .Values.files }} + {{ $key }}: | +{{ toYaml $value | default "{}" | indent 4 }} +{{- end -}} diff --git a/deploy/helm/kibana/templates/deployment.yaml b/deploy/helm/kibana/templates/deployment.yaml new file mode 100755 index 00000000..b1195901 --- /dev/null +++ b/deploy/helm/kibana/templates/deployment.yaml @@ -0,0 +1,251 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{ template "kibana.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "kibana.fullname" . }} +{{- if .Values.deployment.annotations }} + annotations: +{{ toYaml .Values.deployment.annotations | indent 4 }} +{{- end }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "kibana.name" . }} + release: {{ .Release.Name }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | indent 8 }} +{{- end }} + labels: + app: {{ template "kibana.name" . }} + release: "{{ .Release.Name }}" +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} + spec: + serviceAccountName: {{ template "kibana.serviceAccountName" . }} + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} +{{- if or (.Values.initContainers) (.Values.dashboardImport.enabled) (.Values.plugins.enabled) }} + initContainers: +{{- if .Values.initContainers }} +{{- range $key, $value := .Values.initContainers }} + - name: {{ $key | quote }} +{{ toYaml $value | indent 8 }} +{{- end }} +{{- end }} +{{- if .Values.dashboardImport.enabled }} + - name: {{ .Chart.Name }}-dashboardimport + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/bash"] + args: + - "-c" + - "/tmp/dashboardImport.sh" +{{- if .Values.commandline.args }} +{{ toYaml .Values.commandline.args | indent 10 }} +{{- end }} + env: + {{- range $key, $value := .Values.env }} + - name: {{ $key | quote }} + value: {{ tpl $value $ | quote }} + {{- end }} + volumeMounts: + - name: {{ template "kibana.fullname" . }}-dashboards + mountPath: "/kibanadashboards" + - name: {{ template "kibana.fullname" . }}-importscript + mountPath: "/tmp/dashboardImport.sh" + subPath: dashboardImport.sh + {{- range $configFile := (keys .Values.files) }} + - name: {{ template "kibana.name" $ }} + mountPath: "/usr/share/kibana/config/{{ $configFile }}" + subPath: {{ $configFile }} + {{- end }} +{{- end }} +{{- if .Values.plugins.enabled}} + - name: {{ .Chart.Name }}-plugins-install + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - "-c" + - | + set -e + rm -rf plugins/lost+found + plugins=( + {{- range .Values.plugins.values }} + {{ . }} + {{- end }} + ) + if {{ .Values.plugins.reset }} + then + for p in $(./bin/kibana-plugin list | cut -d "@" -f1) + do + ./bin/kibana-plugin remove ${p} + done + fi + for i in "${plugins[@]}" + do + IFS=',' read -ra PLUGIN <<< "$i" + pluginInstalledCheck=$(./bin/kibana-plugin list | grep "${PLUGIN[0]}" | cut -d '@' -f1 || true) + pluginVersionCheck=$(./bin/kibana-plugin list | grep "${PLUGIN[0]}" | cut -d '@' -f2 || true) + if [ "${pluginInstalledCheck}" = "${PLUGIN[0]}" ] + then + if [ "${pluginVersionCheck}" != "${PLUGIN[1]}" ] + then + ./bin/kibana-plugin remove "${PLUGIN[0]}" + ./bin/kibana-plugin install "${PLUGIN[2]}" + fi + else + ./bin/kibana-plugin install "${PLUGIN[2]}" + fi + done + env: + {{- range $key, $value := .Values.env }} + - name: {{ $key | quote }} + value: {{ tpl $value $ | quote }} + {{- end }} + volumeMounts: + - name: plugins + mountPath: /usr/share/kibana/plugins + {{- range $configFile := (keys .Values.files) }} + - name: {{ template "kibana.name" $ }} + mountPath: "/usr/share/kibana/config/{{ $configFile }}" + subPath: {{ $configFile }} + {{- end }} +{{- if .Values.securityContext.enabled }} + securityContext: + allowPrivilegeEscalation: {{ .Values.securityContext.allowPrivilegeEscalation }} +{{- end }} +{{- end }} +{{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.commandline.args }} + args: + - "/bin/bash" + - "/usr/local/bin/kibana-docker" +{{ toYaml .Values.commandline.args | indent 10 }} + {{- end }} + env: + {{- range $key, $value := .Values.env }} + - name: {{ $key | quote }} + value: {{ tpl $value $ | quote }} + {{- end }} +{{- if .Values.envFromSecrets }} + {{- range $key,$value := .Values.envFromSecrets }} + - name: {{ $key | upper | quote}} + valueFrom: + secretKeyRef: + name: {{ $value.from.secret | quote}} + key: {{ $value.from.key | quote}} + {{- end }} +{{- end }} +{{- if (not .Values.authProxyEnabled) }} + ports: + - containerPort: {{ .Values.service.internalPort }} + name: {{ template "kibana.name" . }} + protocol: TCP +{{- end }} +{{- if .Values.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: {{ .Values.livenessProbe.path }} + port: {{ .Values.service.internalPort }} + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} +{{- end }} +{{- if .Values.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: {{ .Values.readinessProbe.path }} + port: {{ .Values.service.internalPort }} + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + successThreshold: {{ .Values.readinessProbe.successThreshold }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} +{{- end }} + resources: +{{ toYaml .Values.resources | indent 10 }} + volumeMounts: + {{- range $configFile := (keys .Values.files) }} + - name: {{ template "kibana.name" $ }} + mountPath: "/usr/share/kibana/config/{{ $configFile }}" + subPath: {{ $configFile }} + {{- end }} +{{- if .Values.extraVolumeMounts }} +{{ toYaml .Values.extraVolumeMounts | indent 8 }} +{{- end }} +{{- if .Values.plugins.enabled}} + - name: plugins + mountPath: /usr/share/kibana/plugins +{{- end }} +{{- with .Values.extraContainers }} +{{ tpl . $ | indent 6 }} +{{- end }} +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath }} +{{- end }} + {{- if .Values.image.pullSecrets }} + imagePullSecrets: +{{ toYaml .Values.image.pullSecrets | indent 8 }} + {{- end }} + {{- if .Values.affinity }} + affinity: +{{ toYaml .Values.affinity | indent 8 }} + {{- end }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} + tolerations: +{{ toYaml .Values.tolerations | indent 8 }} +{{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + fsGroup: {{ .Values.securityContext.fsGroup }} +{{- end }} + volumes: + - name: {{ template "kibana.name" . }} + configMap: + name: {{ template "kibana.fullname" . }} +{{- if .Values.plugins.enabled}} + - name: plugins + {{- if .Values.persistentVolumeClaim.enabled }} + persistentVolumeClaim: + claimName: {{ template "kibana.fullname" . }} + {{- else }} + emptyDir: {} + {{- end }} +{{- end }} +{{- if .Values.dashboardImport.enabled }} + - name: {{ template "kibana.fullname" . }}-dashboards + configMap: + name: {{ template "kibana.fullname" . }}-dashboards + - name: {{ template "kibana.fullname" . }}-importscript + configMap: + name: {{ template "kibana.fullname" . }}-importscript + defaultMode: 0777 +{{- end }} +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + configMap: + name: {{ .configMap }} +{{- end }} +{{- if .Values.extraVolumes }} +{{ toYaml .Values.extraVolumes | indent 8 }} +{{- end }} diff --git a/deploy/helm/kibana/templates/ingress.yaml b/deploy/helm/kibana/templates/ingress.yaml new file mode 100755 index 00000000..df3cc9f5 --- /dev/null +++ b/deploy/helm/kibana/templates/ingress.yaml @@ -0,0 +1,37 @@ +{{- if .Values.ingress.enabled -}} +{{- $serviceName := include "kibana.fullname" . -}} +{{- $servicePort := .Values.service.externalPort -}} +{{- if semverCompare ">=1.14" .Capabilities.KubeVersion.GitVersion }} +apiVersion: networking.k8s.io/v1beta1 +{{ else }} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + labels: + app: {{ template "kibana.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "kibana.fullname" . }} + annotations: + {{- range $key, $value := .Values.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} +spec: + rules: + {{- range .Values.ingress.hosts }} + {{- $url := splitList "/" . }} + - host: {{ first $url }} + http: + paths: + - path: /{{ rest $url | join "/" }} + backend: + serviceName: {{ $serviceName }} + servicePort: {{ $servicePort }} + {{- end -}} + {{- if .Values.ingress.tls }} + tls: +{{ toYaml .Values.ingress.tls | indent 4 }} + {{- end -}} +{{- end -}} diff --git a/deploy/helm/kibana/templates/service.yaml b/deploy/helm/kibana/templates/service.yaml new file mode 100755 index 00000000..4416c45b --- /dev/null +++ b/deploy/helm/kibana/templates/service.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: {{ template "kibana.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- range $key, $value := .Values.service.labels }} + {{ $key }}: {{ $value | quote }} + {{- end }} + name: {{ template "kibana.fullname" . }} + {{- with .Values.service.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + {{- if .Values.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- range $cidr := .Values.service.loadBalancerSourceRanges }} + - {{ $cidr }} + {{- end }} + {{- end }} + type: {{ .Values.service.type }} + {{- if and (eq .Values.service.type "ClusterIP") .Values.service.clusterIP }} + clusterIP: {{ .Values.service.clusterIP }} + {{- end }} + ports: + - port: {{ .Values.service.externalPort }} +{{- if not .Values.authProxyEnabled }} + targetPort: {{ .Values.service.internalPort }} +{{- else }} + targetPort: {{ .Values.service.authProxyPort }} +{{- end }} + protocol: TCP +{{ if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} + nodePort: {{ .Values.service.nodePort }} +{{ end }} +{{- if .Values.service.portName }} + name: {{ .Values.service.portName }} +{{- end }} +{{- if .Values.service.externalIPs }} + externalIPs: +{{ toYaml .Values.service.externalIPs | indent 4 }} +{{- end }} + selector: + app: {{ template "kibana.name" . }} + release: {{ .Release.Name }} +{{- range $key, $value := .Values.service.selector }} + {{ $key }}: {{ $value | quote }} +{{- end }} +{{- if .Values.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} +{{- end }} diff --git a/deploy/helm/kibana/templates/serviceaccount.yaml b/deploy/helm/kibana/templates/serviceaccount.yaml new file mode 100755 index 00000000..948390ab --- /dev/null +++ b/deploy/helm/kibana/templates/serviceaccount.yaml @@ -0,0 +1,11 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "kibana.serviceAccountName" . }} + labels: + app: {{ template "kibana.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} +{{- end -}} diff --git a/deploy/helm/kibana/templates/tests/test-configmap.yaml b/deploy/helm/kibana/templates/tests/test-configmap.yaml new file mode 100755 index 00000000..ca67f961 --- /dev/null +++ b/deploy/helm/kibana/templates/tests/test-configmap.yaml @@ -0,0 +1,37 @@ +{{- if .Values.testFramework.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "kibana.fullname" . }}-test + labels: + app: {{ template "kibana.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: "{{ .Release.Service }}" + release: "{{ .Release.Name }}" +data: + run.sh: |- + @test "Test Status" { + {{- if .Values.service.selector }} + skip "Can't guarentee pod names with selector" + {{- else }} + {{- $port := .Values.service.externalPort }} + url="http://{{ template "kibana.fullname" . }}{{ if $port }}:{{ $port }}{{ end }}/api{{ .Values.livenessProbe.path }}" + + # retry for 1 minute + run curl -s -o /dev/null -I -w "%{http_code}" --retry 30 --retry-delay 2 $url + + code=$(curl -s -o /dev/null -I -w "%{http_code}" $url) + body=$(curl $url) + if [ "$code" == "503" ] + then + skip "Kibana Unavailable (503), can't get status - see pod logs: $body" + fi + + result=$(echo $body | jq -cr '.status.statuses[]') + [ "$result" != "" ] + + result=$(echo $body | jq -cr '.status.statuses[] | select(.state != "green")') + [ "$result" == "" ] + {{- end }} + } +{{- end }} diff --git a/deploy/helm/kibana/templates/tests/test.yaml b/deploy/helm/kibana/templates/tests/test.yaml new file mode 100755 index 00000000..74d5827d --- /dev/null +++ b/deploy/helm/kibana/templates/tests/test.yaml @@ -0,0 +1,44 @@ +{{- if .Values.testFramework.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ template "kibana.fullname" . }}-test + labels: + app: {{ template "kibana.fullname" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + heritage: "{{ .Release.Service }}" + release: "{{ .Release.Name }}" + annotations: + "helm.sh/hook": test-success +spec: + initContainers: + - name: test-framework + image: "{{ .Values.testFramework.image}}:{{ .Values.testFramework.tag }}" + command: + - "bash" + - "-c" + - | + set -ex + # copy bats to tools dir + cp -R /usr/local/libexec/ /tools/bats/ + volumeMounts: + - mountPath: /tools + name: tools + containers: + - name: {{ .Release.Name }}-test + image: "dwdraju/alpine-curl-jq" + command: ["/tools/bats/bats", "-t", "/tests/run.sh"] + volumeMounts: + - mountPath: /tests + name: tests + readOnly: true + - mountPath: /tools + name: tools + volumes: + - name: tests + configMap: + name: {{ template "kibana.fullname" . }}-test + - name: tools + emptyDir: {} + restartPolicy: Never +{{- end }} diff --git a/deploy/helm/kibana/templates/volume-claim.yaml b/deploy/helm/kibana/templates/volume-claim.yaml new file mode 100755 index 00000000..29397125 --- /dev/null +++ b/deploy/helm/kibana/templates/volume-claim.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.plugins.enabled .Values.persistentVolumeClaim.enabled -}} +{{- if not .Values.persistentVolumeClaim.existingClaim -}} +apiVersion: "v1" +kind: "PersistentVolumeClaim" +metadata: +{{- if .Values.persistentVolumeClaim.annotations }} + annotations: +{{ toYaml .Values.persistentVolumeClaim.annotations | indent 4 }} +{{- end }} + labels: + app: {{ template "kibana.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + component: "{{ .Values.persistentVolumeClaim.name }}" + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + name: {{ template "kibana.fullname" . }} +spec: + accessModes: +{{ toYaml .Values.persistentVolumeClaim.accessModes | indent 4 }} +{{- if .Values.persistentVolumeClaim.storageClass }} +{{- if (eq "-" .Values.persistentVolumeClaim.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistentVolumeClaim.storageClass }}" +{{- end }} +{{- end }} + resources: + requests: + storage: "{{ .Values.persistentVolumeClaim.size }}" +{{- end -}} +{{- end -}} diff --git a/deploy/helm/kibana/values.yaml b/deploy/helm/kibana/values.yaml new file mode 100755 index 00000000..c0cd6e02 --- /dev/null +++ b/deploy/helm/kibana/values.yaml @@ -0,0 +1,241 @@ +image: + repository: "docker.elastic.co/kibana/kibana-oss" + tag: "6.7.0" + pullPolicy: "IfNotPresent" + +testFramework: + enabled: "true" + image: "dduportal/bats" + tag: "0.4.0" + +commandline: + args: [] + +env: {} + ## All Kibana configuration options are adjustable via env vars. + ## To adjust a config option to an env var uppercase + replace `.` with `_` + ## Ref: https://www.elastic.co/guide/en/kibana/current/settings.html + ## For kibana < 6.6, use ELASTICSEARCH_URL instead + # ELASTICSEARCH_HOSTS: http://elasticsearch-client:9200 + # SERVER_PORT: 5601 + # LOGGING_VERBOSE: "true" + # SERVER_DEFAULTROUTE: "/app/kibana" + +envFromSecrets: {} + ## Create a secret manually. Reference it here to inject environment variables + # ELASTICSEARCH_USERNAME: + # from: + # secret: secret-name-here + # key: ELASTICSEARCH_USERNAME + # ELASTICSEARCH_PASSWORD: + # from: + # secret: secret-name-here + # key: ELASTICSEARCH_PASSWORD + +files: + kibana.yml: + ## Default Kibana configuration from kibana-docker. + server.name: kibana + server.host: "0" + ## For kibana < 6.6, use elasticsearch.url instead + elasticsearch.hosts: http://elasticsearch:9200 + + ## Custom config properties below + ## Ref: https://www.elastic.co/guide/en/kibana/current/settings.html + # server.port: 5601 + # logging.verbose: "true" + # server.defaultRoute: "/app/kibana" + +deployment: + annotations: {} + +service: + type: ClusterIP + # clusterIP: None + # portName: kibana-svc + externalPort: 443 + internalPort: 5601 + # authProxyPort: 5602 To be used with authProxyEnabled and a proxy extraContainer + ## External IP addresses of service + ## Default: nil + ## + # externalIPs: + # - 192.168.0.1 + # + ## LoadBalancer IP if service.type is LoadBalancer + ## Default: nil + ## + # loadBalancerIP: 10.2.2.2 + annotations: {} + # Annotation example: setup ssl with aws cert when service.type is LoadBalancer + # service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:us-east-1:EXAMPLE_CERT + labels: {} + ## Label example: show service URL in `kubectl cluster-info` + # kubernetes.io/cluster-service: "true" + ## Limit load balancer source ips to list of CIDRs (where available) + # loadBalancerSourceRanges: [] + selector: {} + +ingress: + enabled: false + # hosts: + # - kibana.localhost.localdomain + # - localhost.localdomain/kibana + # annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # tls: + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +serviceAccount: + # Specifies whether a service account should be created + create: false + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + # If set and create is false, the service account must be existing + name: + +livenessProbe: + enabled: false + path: /status + initialDelaySeconds: 30 + timeoutSeconds: 10 + +readinessProbe: + enabled: false + path: /status + initialDelaySeconds: 30 + timeoutSeconds: 10 + periodSeconds: 10 + successThreshold: 5 + +# Enable an authproxy. Specify container in extraContainers +authProxyEnabled: false + +extraContainers: | +# - name: proxy +# image: quay.io/gambol99/keycloak-proxy:latest +# args: +# - --resource=uri=/* +# - --discovery-url=https://discovery-url +# - --client-id=client +# - --client-secret=secret +# - --listen=0.0.0.0:5602 +# - --upstream-url=http://127.0.0.1:5601 +# ports: +# - name: web +# containerPort: 9090 + +extraVolumeMounts: [] + +extraVolumes: [] + +resources: {} + # limits: + # cpu: 100m + # memory: 300Mi + # requests: + # cpu: 100m + # memory: 300Mi + +priorityClassName: "" + +# Affinity for pod assignment +# Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +# affinity: {} + +# Tolerations for pod assignment +# Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +tolerations: [] + +# Node labels for pod assignment +# Ref: https://kubernetes.io/docs/user-guide/node-selection/ +nodeSelector: {} + +podAnnotations: {} +replicaCount: 1 +revisionHistoryLimit: 3 + +# Custom labels for pod assignment +podLabels: {} + +# To export a dashboard from a running Kibana 6.3.x use: +# curl --user : -XGET https://kibana.yourdomain.com:5601/api/kibana/dashboards/export?dashboard= > my-dashboard.json +# A dashboard is defined by a name and a string with the json payload or the download url +dashboardImport: + enabled: false + timeout: 60 + basePath: / + xpackauth: + enabled: false + username: myuser + password: mypass + dashboards: {} + # k8s: https://raw.githubusercontent.com/monotek/kibana-dashboards/master/k8s-fluentd-elasticsearch.json + +# List of plugins to install using initContainer +# NOTE : We notice that lower resource constraints given to the chart + plugins are likely not going to work well. +plugins: + # set to true to enable plugins installation + enabled: false + # set to true to remove all kibana plugins before installation + reset: false + # Use to add/upgrade plugin + values: + # - elastalert-kibana-plugin,1.0.1,https://github.com/bitsensor/elastalert-kibana-plugin/releases/download/1.0.1/elastalert-kibana-plugin-1.0.1-6.4.2.zip + # - logtrail,0.1.31,https://github.com/sivasamyk/logtrail/releases/download/v0.1.31/logtrail-6.6.0-0.1.31.zip + # - other_plugin + +persistentVolumeClaim: + # set to true to use pvc + enabled: false + # set to true to use you own pvc + existingClaim: false + annotations: {} + + accessModes: + - ReadWriteOnce + size: "5Gi" + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + +# default security context +securityContext: + enabled: false + allowPrivilegeEscalation: false + runAsUser: 1000 + fsGroup: 2000 + +extraConfigMapMounts: [] + # - name: logtrail-configs + # configMap: kibana-logtrail + # mountPath: /usr/share/kibana/plugins/logtrail/logtrail.json + # subPath: logtrail.json + +# Add your own init container or uncomment and modify the given example. +initContainers: {} + ## Don't start kibana till Elasticsearch is reachable. + ## Ensure that it is available at http://elasticsearch:9200 + ## + # es-check: # <- will be used as container name + # image: "appropriate/curl:latest" + # imagePullPolicy: "IfNotPresent" + # command: + # - "/bin/sh" + # - "-c" + # - | + # is_down=true + # while "$is_down"; do + # if curl -sSf --fail-early --connect-timeout 5 http://elasticsearch:9200; then + # is_down=false + # else + # sleep 5 + # fi + # done diff --git a/deploy/helm/mssql-data/templates/deployment.yaml b/deploy/helm/mssql-data/templates/deployment.yaml index 43cdd132..8c528e9a 100644 --- a/deploy/helm/mssql-data/templates/deployment.yaml +++ b/deploy/helm/mssql-data/templates/deployment.yaml @@ -9,25 +9,25 @@ parameters: kind: Managed --- -kind: StorageClass -apiVersion: storage.k8s.io/v1 -metadata: - name: {{ .Values.storage.disk_data }} -provisioner: kubernetes.io/azure-disk -parameters: - storageaccounttype: Standard_LRS - kind: Managed +# kind: StorageClass +# apiVersion: storage.k8s.io/v1 +# metadata: +# name: {{ .Values.storage.disk_data }} +# provisioner: kubernetes.io/azure-disk +# parameters: +# storageaccounttype: Standard_LRS +# kind: Managed ---- -kind: StorageClass -apiVersion: storage.k8s.io/v1 -metadata: - name: {{ .Values.storage.disk_log}} -provisioner: kubernetes.io/azure-disk -parameters: - storageaccounttype: Standard_LRS - kind: Managed ---- +# --- +# kind: StorageClass +# apiVersion: storage.k8s.io/v1 +# metadata: +# name: {{ .Values.storage.disk_log}} +# provisioner: kubernetes.io/azure-disk +# parameters: +# storageaccounttype: Standard_LRS +# kind: Managed +# --- kind: PersistentVolumeClaim apiVersion: v1 metadata: @@ -41,32 +41,32 @@ spec: requests: storage: {{ .Values.storage.disk_system_size }} --- -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - name: mssql-pv-data-claim - annotations: - volume.beta.kubernetes.io/storage-class: {{ .Values.storage.disk_data }} -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.storage.disk_data_size }} ---- -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - name: mssql-pv-log-claim - annotations: - volume.beta.kubernetes.io/storage-class: {{ .Values.storage.disk_log }} -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.storage.disk_log_size }} ---- +# kind: PersistentVolumeClaim +# apiVersion: v1 +# metadata: +# name: mssql-pv-data-claim +# annotations: +# volume.beta.kubernetes.io/storage-class: {{ .Values.storage.disk_data }} +# spec: +# accessModes: +# - ReadWriteOnce +# resources: +# requests: +# storage: {{ .Values.storage.disk_data_size }} +# --- +# kind: PersistentVolumeClaim +# apiVersion: v1 +# metadata: +# name: mssql-pv-log-claim +# annotations: +# volume.beta.kubernetes.io/storage-class: {{ .Values.storage.disk_log }} +# spec: +# accessModes: +# - ReadWriteOnce +# resources: +# requests: +# storage: {{ .Values.storage.disk_log_size }} +# --- apiVersion: apps/v1 kind: Deployment @@ -90,6 +90,10 @@ spec: labels: {{- include "mssql-data.selectorLabels" . | nindent 8 }} spec: + terminationGracePeriodSeconds: 30 + hostname: mssqlinst + securityContext: + fsGroup: 10001 {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} @@ -127,20 +131,20 @@ spec: volumeMounts: - name: system mountPath: /var/opt/mssql - - name: data - mountPath: /data - - name: log - mountPath: /log + # - name: data + # mountPath: /data + # - name: log + # mountPath: /log volumes: - name: system persistentVolumeClaim: claimName: mssql-pv-system-claim - - name: data - persistentVolumeClaim: - claimName: mssql-pv-data-claim - - name: log - persistentVolumeClaim: - claimName: mssql-pv-log-claim + # - name: data + # persistentVolumeClaim: + # claimName: mssql-pv-data-claim + # - name: log + # persistentVolumeClaim: + # claimName: mssql-pv-log-claim {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/deploy/helm/mssql-data/values.yaml b/deploy/helm/mssql-data/values.yaml index f97dba77..dc36e9ec 100644 --- a/deploy/helm/mssql-data/values.yaml +++ b/deploy/helm/mssql-data/values.yaml @@ -60,23 +60,23 @@ ingress: storage: name: mssql-persistent-storage disk_system: azure-mssql-system - disk_system_size: 6Gi - disk_data: azure-mssql-data - disk_data_size: 2Gi - disk_log: azure-mssql-log - disk_log_size: 2Gi + disk_system_size: 8Gi + # disk_data: azure-mssql-data + # disk_data_size: 2Gi + # disk_log: azure-mssql-log + # disk_log_size: 2Gi env: values: - name: MSSQL_AGENT_ENABLED value: 'true' - - name: MSSQL_DATA_DIR - value: '/data' - - name: MSSQL_LOG_DIT - value: '/logs' + # - name: MSSQL_DATA_DIR + # value: '/data' + # - name: MSSQL_LOG_DIT + # value: '/logs' - name: ACCEPT_EULA value: "Y" - - name: MSSQL_PID - value: Developer + # - name: MSSQL_PID + # value: Developer - name: MSSQL_SA_PASSWORD value: yourStrong(!)Password diff --git a/deploy/helm/o2bionics-webapp/dev.values.yaml b/deploy/helm/o2bionics-webapp/dev.values.yaml new file mode 100644 index 00000000..1fdac07d --- /dev/null +++ b/deploy/helm/o2bionics-webapp/dev.values.yaml @@ -0,0 +1,83 @@ +# Default values for o2bus-webapp. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: o2bionics.o2bus.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-secret-o2bionics-com + hosts: + - o2bionics.o2bus.com + +resources: #{} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/o2bionics-webapp/values.yaml b/deploy/helm/o2bionics-webapp/values.yaml index a2a42fb7..aaf9895a 100644 --- a/deploy/helm/o2bionics-webapp/values.yaml +++ b/deploy/helm/o2bionics-webapp/values.yaml @@ -48,10 +48,6 @@ ingress: kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - host: o2bionics.o2bus.com - paths: - - path: / - pathType: ImplementationSpecific - host: o2bionics.com paths: - path: / @@ -59,7 +55,6 @@ ingress: tls: - secretName: tls-secret-o2bionics-com hosts: - - o2bionics.o2bus.com - o2bionics.com resources: #{} diff --git a/deploy/helm/o2bus-webapp/dev.values.yaml b/deploy/helm/o2bus-webapp/dev.values.yaml new file mode 100644 index 00000000..c663ef3a --- /dev/null +++ b/deploy/helm/o2bus-webapp/dev.values.yaml @@ -0,0 +1,94 @@ +# Default values for o2bus-webapp. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + kubernetes.io/ingress.global-static-ip-name: webapp-ipv6 + acme.cert-manager.io/http01-edit-in-place: "true" + hosts: + - host: o2bus.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-o2bus-com + hosts: + - o2bus.com + +resources: #{} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +health: + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/o2bus-webapp/templates/deployment.yaml b/deploy/helm/o2bus-webapp/templates/deployment.yaml index 8451c3e8..4625ab6c 100644 --- a/deploy/helm/o2bus-webapp/templates/deployment.yaml +++ b/deploy/helm/o2bus-webapp/templates/deployment.yaml @@ -39,12 +39,12 @@ spec: protocol: TCP livenessProbe: httpGet: - path: / - port: http + path: {{ .Values.health.livenessProbe.httpGet.path }} + port: {{ .Values.health.livenessProbe.httpGet.port }} readinessProbe: httpGet: - path: / - port: http + path: {{ .Values.health.readinessProbe.httpGet.path }} + port: {{ .Values.health.readinessProbe.httpGet.port }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} diff --git a/deploy/helm/o2bus-webapp/values.yaml b/deploy/helm/o2bus-webapp/values.yaml index 65a47277..6b95226f 100644 --- a/deploy/helm/o2bus-webapp/values.yaml +++ b/deploy/helm/o2bus-webapp/values.yaml @@ -1,4 +1,4 @@ -# Default values for o2bus-webapp. +# Default values for o2nextgen-webapp. # This is a YAML-formatted file. # Declare variables to be passed into your templates. @@ -46,16 +46,17 @@ ingress: annotations: cert-manager.io/cluster-issuer: letsencrypt kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" + kubernetes.io/ingress.global-static-ip-name: webapp-ipv6 + acme.cert-manager.io/http01-edit-in-place: "true" hosts: - - host: o2bus.com + - host: o2nextgen.com paths: - path: / pathType: ImplementationSpecific tls: - - secretName: tls-secret-o2bus + - secretName: tls-o2bus-com hosts: - - o2bus.com + - o2nextgen.com resources: #{} # We usually recommend not to specify default resources and to leave this as a conscious @@ -69,6 +70,16 @@ resources: #{} # cpu: 100m # memory: 128Mi +health: + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + autoscaling: enabled: false minReplicas: 1 diff --git a/deploy/helm/ontracker-api/dev.value.yaml b/deploy/helm/ontracker-api/dev.value.yaml new file mode 100644 index 00000000..d6b725d5 --- /dev/null +++ b/deploy/helm/ontracker-api/dev.value.yaml @@ -0,0 +1,88 @@ +# Default values for ontracker-api. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: ontracker-api.o2bus.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-secret-o2bus-ontracker-api + hosts: + - ontracker-api.o2bus.com +# env: +# values: +# - name: CHOKIDAR_USEPOLLING +# value: true +# - name: REACT_APP_NAME +# value: dockerEnv +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/ontracker-api/templates/deployment.yaml b/deploy/helm/ontracker-api/templates/deployment.yaml index 2992a810..c806c149 100644 --- a/deploy/helm/ontracker-api/templates/deployment.yaml +++ b/deploy/helm/ontracker-api/templates/deployment.yaml @@ -37,14 +37,21 @@ spec: - name: http containerPort: 80 protocol: TCP - # livenessProbe: - # httpGet: - # path: /api/version - # port: http - # readinessProbe: - # httpGet: - # path: /api/version - # port: http + env: + {{- if .Values.env.values -}} + {{- range .Values.env.values }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + livenessProbe: + httpGet: + path: /api/version + port: http + readinessProbe: + httpGet: + path: /api/version + port: http resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} diff --git a/deploy/helm/ontracker-api/values.yaml b/deploy/helm/ontracker-api/values.yaml index d6b725d5..f4dc556c 100644 --- a/deploy/helm/ontracker-api/values.yaml +++ b/deploy/helm/ontracker-api/values.yaml @@ -48,20 +48,22 @@ ingress: kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - host: ontracker-api.o2bus.com + - host: ontracker-api.o2nextgen.com paths: - path: / pathType: ImplementationSpecific tls: - - secretName: tls-secret-o2bus-ontracker-api + - secretName: tls-secret-o2nextgen-ontracker-api hosts: - - ontracker-api.o2bus.com -# env: -# values: -# - name: CHOKIDAR_USEPOLLING -# value: true -# - name: REACT_APP_NAME -# value: dockerEnv + - ontracker-api.o2nextgen.com +env: + values: + - name: ConnectionString + value: Server=tcp:mssql-data;Initial Catalog=O2Bionics.O2NextGen.OnTrackerDb;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30; + - name: DatabaseInitializerSettings__Initialize + value: false + - name : GeoDatabase__ConnectionDb + value: /geoip/GeoLite2-City.mmdb resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little diff --git a/deploy/helm/pfr-mvc/.helmignore b/deploy/helm/pfr-mvc/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/deploy/helm/pfr-mvc/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/helm/pfr-mvc/Chart.yaml b/deploy/helm/pfr-mvc/Chart.yaml new file mode 100644 index 00000000..cb4bcba9 --- /dev/null +++ b/deploy/helm/pfr-mvc/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: pfr-mvc +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/deploy/helm/pfr-mvc/templates/NOTES.txt b/deploy/helm/pfr-mvc/templates/NOTES.txt new file mode 100644 index 00000000..1d27a41a --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "pfr-mvc.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "pfr-mvc.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "pfr-mvc.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "pfr-mvc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/helm/pfr-mvc/templates/_helpers.tpl b/deploy/helm/pfr-mvc/templates/_helpers.tpl new file mode 100644 index 00000000..149e6fb1 --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "pfr-mvc.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "pfr-mvc.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "pfr-mvc.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "pfr-mvc.labels" -}} +helm.sh/chart: {{ include "pfr-mvc.chart" . }} +{{ include "pfr-mvc.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "pfr-mvc.selectorLabels" -}} +app.kubernetes.io/name: {{ include "pfr-mvc.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "pfr-mvc.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "pfr-mvc.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/pfr-mvc/templates/deployment.yaml b/deploy/helm/pfr-mvc/templates/deployment.yaml new file mode 100644 index 00000000..c882ed67 --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "pfr-mvc.fullname" . }} + labels: + {{- include "pfr-mvc.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "pfr-mvc.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "pfr-mvc.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "pfr-mvc.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- if .Values.env.values -}} + {{- range .Values.env.values }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/pfr-mvc/templates/hpa.yaml b/deploy/helm/pfr-mvc/templates/hpa.yaml new file mode 100644 index 00000000..e23a6037 --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "pfr-mvc.fullname" . }} + labels: + {{- include "pfr-mvc.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "pfr-mvc.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deploy/helm/pfr-mvc/templates/ingress.yaml b/deploy/helm/pfr-mvc/templates/ingress.yaml new file mode 100644 index 00000000..2a232008 --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "pfr-mvc.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "pfr-mvc.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/pfr-mvc/templates/service.yaml b/deploy/helm/pfr-mvc/templates/service.yaml new file mode 100644 index 00000000..c9632ec9 --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "pfr-mvc.fullname" . }} + labels: + {{- include "pfr-mvc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "pfr-mvc.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/pfr-mvc/templates/serviceaccount.yaml b/deploy/helm/pfr-mvc/templates/serviceaccount.yaml new file mode 100644 index 00000000..821ebd12 --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "pfr-mvc.serviceAccountName" . }} + labels: + {{- include "pfr-mvc.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/pfr-mvc/templates/tests/test-connection.yaml b/deploy/helm/pfr-mvc/templates/tests/test-connection.yaml new file mode 100644 index 00000000..dd08c0b8 --- /dev/null +++ b/deploy/helm/pfr-mvc/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "pfr-mvc.fullname" . }}-test-connection" + labels: + {{- include "pfr-mvc.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "pfr-mvc.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deploy/helm/pfr-mvc/values.yaml b/deploy/helm/pfr-mvc/values.yaml new file mode 100644 index 00000000..dfe6df1b --- /dev/null +++ b/deploy/helm/pfr-mvc/values.yaml @@ -0,0 +1,104 @@ +# Default values for pfr-mvc. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" + # nginx.ingress.kubernetes.io/proxy-buffering: "on" + # nginx.ingress.kubernetes.io/proxy-body-size: 16m + # nginx.ingress.kubernetes.io/proxy-buffer-size: 8k + # nginx.ingress.kubernetes.io/proxy-busy-buffers-size: 16k + # nginx.ingress.kubernetes.io/client-body-buffer-size: 16m + # nginx.ingress.kubernetes.io/large-client-header-buffers: 4 100k + # nginx.ingress.kubernetes.io/load-balance: ewma + # nginx.ingress.kubernetes.io/proxy-read-timeout: "1000" + # nginx.ingress.kubernetes.io/proxy-send-timeout: "1000" + # nginx.ingress.kubernetes.io/server-snippet: | # this is where the magic happens + # client_header_buffer_size 100k; + # large_client_header_buffers 4 100k; + # fastcgi_buffers 16 16k; + # fastcgi_buffer_size 32k; + hosts: + - host: app.pfr-centr.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: app-pfr-mvc-tls + hosts: + - app.pfr-centr.com +env: + values: + - name: Services__AuthApiUrl + value: https://auth.o2nextgen.com + - name: Services__CallBackUrl + value: https://app.pfr-centr.com + - name: Services__CGenApiUrl + value: https://cgen-api.o2nextgen.com +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/pfr-webapp/dev.value.yaml b/deploy/helm/pfr-webapp/dev.value.yaml new file mode 100644 index 00000000..c486590b --- /dev/null +++ b/deploy/helm/pfr-webapp/dev.value.yaml @@ -0,0 +1,90 @@ +# Default values for pfr-webapp. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + kubernetes.io/ingress.global-static-ip-name: webapp-ipv6 + certmanager.k8s.io/cluster-issuer: letsencrypt + certmanager.k8s.io/acme-http01-edit-in-place: "true" + kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: pfr-webapp.o2bus.com + paths: + - path: / + pathType: ImplementationSpecific + - host: pfr-centr.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-secret-pfr-webapp-com + hosts: + - pfr-webapp.o2bus.com + - pfr-centr.com + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/pfr-webapp/values.yaml b/deploy/helm/pfr-webapp/values.yaml index 3fffaf7f..2112e34c 100644 --- a/deploy/helm/pfr-webapp/values.yaml +++ b/deploy/helm/pfr-webapp/values.yaml @@ -46,20 +46,18 @@ ingress: annotations: cert-manager.io/cluster-issuer: letsencrypt kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" + # kubernetes.io/ingress.global-static-ip-name: webapp-ipv6 + # acme.cert-manager.io/http01-edit-in-place: "true" + # nginx.ingress.kubernetes.io/rewrite-target: / + cert-manager.io/acme-challenge-type: http01 hosts: - # - host: pfr-webapp.o2bus.com - # paths: - # - path: / - # pathType: ImplementationSpecific - host: pfr-centr.com paths: - path: / pathType: ImplementationSpecific - tls: + tls: - secretName: tls-secret-pfr-webapp-com hosts: - # - pfr-webapp.o2bus.com - pfr-centr.com resources: {} diff --git a/deploy/helm/rabbitmq/.helmignore b/deploy/helm/rabbitmq/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/deploy/helm/rabbitmq/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/helm/rabbitmq/Chart.yaml b/deploy/helm/rabbitmq/Chart.yaml new file mode 100644 index 00000000..a54865fc --- /dev/null +++ b/deploy/helm/rabbitmq/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: rabbitmq +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/deploy/helm/rabbitmq/templates/NOTES.txt b/deploy/helm/rabbitmq/templates/NOTES.txt new file mode 100644 index 00000000..d19b2c38 --- /dev/null +++ b/deploy/helm/rabbitmq/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "rabbitmq.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "rabbitmq.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "rabbitmq.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "rabbitmq.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/helm/rabbitmq/templates/_helpers.tpl b/deploy/helm/rabbitmq/templates/_helpers.tpl new file mode 100644 index 00000000..0e218d7f --- /dev/null +++ b/deploy/helm/rabbitmq/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "rabbitmq.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "rabbitmq.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "rabbitmq.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "rabbitmq.labels" -}} +helm.sh/chart: {{ include "rabbitmq.chart" . }} +{{ include "rabbitmq.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "rabbitmq.selectorLabels" -}} +app.kubernetes.io/name: {{ include "rabbitmq.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "rabbitmq.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "rabbitmq.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/rabbitmq/templates/deployment.yaml b/deploy/helm/rabbitmq/templates/deployment.yaml new file mode 100644 index 00000000..395dd220 --- /dev/null +++ b/deploy/helm/rabbitmq/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "rabbitmq.fullname" . }} + labels: + {{- include "rabbitmq.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "rabbitmq.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "rabbitmq.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "rabbitmq.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + protocol: TCP + containerPort: 15672 + - name: amqp + protocol: TCP + containerPort: 5672 + livenessProbe: + exec: + command: + - "rabbitmq-diagnostics" + - "ping" + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 15 + readinessProbe: + exec: + command: + - "rabbitmq-diagnostics" + - "check_port_connectivity" + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 15 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/rabbitmq/templates/hpa.yaml b/deploy/helm/rabbitmq/templates/hpa.yaml new file mode 100644 index 00000000..3411a1fb --- /dev/null +++ b/deploy/helm/rabbitmq/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "rabbitmq.fullname" . }} + labels: + {{- include "rabbitmq.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "rabbitmq.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deploy/helm/rabbitmq/templates/ingress.yaml b/deploy/helm/rabbitmq/templates/ingress.yaml new file mode 100644 index 00000000..6d2a3fb9 --- /dev/null +++ b/deploy/helm/rabbitmq/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "rabbitmq.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "rabbitmq.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/rabbitmq/templates/service.yaml b/deploy/helm/rabbitmq/templates/service.yaml new file mode 100644 index 00000000..4621fc52 --- /dev/null +++ b/deploy/helm/rabbitmq/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "rabbitmq.fullname" . }} + labels: + {{- include "rabbitmq.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: 5672 + targetPort: 5672 + name: amqp + protocol: TCP + - port: 15672 + targetPort: 15672 + name: http + protocol: TCP + selector: + {{- include "rabbitmq.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/rabbitmq/templates/serviceaccount.yaml b/deploy/helm/rabbitmq/templates/serviceaccount.yaml new file mode 100644 index 00000000..b0221e04 --- /dev/null +++ b/deploy/helm/rabbitmq/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "rabbitmq.serviceAccountName" . }} + labels: + {{- include "rabbitmq.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/rabbitmq/templates/tests/test-connection.yaml b/deploy/helm/rabbitmq/templates/tests/test-connection.yaml new file mode 100644 index 00000000..302f4f4b --- /dev/null +++ b/deploy/helm/rabbitmq/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "rabbitmq.fullname" . }}-test-connection" + labels: + {{- include "rabbitmq.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "rabbitmq.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deploy/helm/rabbitmq/values.yaml b/deploy/helm/rabbitmq/values.yaml new file mode 100644 index 00000000..76a3ef49 --- /dev/null +++ b/deploy/helm/rabbitmq/values.yaml @@ -0,0 +1,82 @@ +# Default values for rabbitmq. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: rabbitmq + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "3-management" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: LoadBalancer + port: 15672 + +ingress: + enabled: false + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + hosts: [] + # - host: rmq.o2bus.com + # paths: + # - path: / + # pathType: ImplementationSpecific + tls: [] + # - secretName: tls-rmq-o2bus.com + # hosts: + # - rmq.o2bus.com + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/helm/test-app/templates/tests/test-connection.yaml b/deploy/helm/test-app/templates/tests/test-connection.yaml new file mode 100644 index 00000000..f7bc8d10 --- /dev/null +++ b/deploy/helm/test-app/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "test-app.fullname" . }}-test-connection" + labels: + {{- include "test-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "test-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deploy/release-deploy/clouds/azure/terraform/tls-issuer.sh b/deploy/release-deploy/clouds/azure/terraform/tls-issuer.sh index 4e51b9de..8cd6ff8e 100644 --- a/deploy/release-deploy/clouds/azure/terraform/tls-issuer.sh +++ b/deploy/release-deploy/clouds/azure/terraform/tls-issuer.sh @@ -1,6 +1,6 @@ LETS_ENCRYPT_EMAIL=o2bionics@hotmail.com cat <<-EOF | kubectl apply --namespace default -f - -apiVersion: cert-manager.io/v1 +apiVersion: cert-manager.io/v1alpha2 kind: ClusterIssuer metadata: diff --git a/deploy/release-deploy/helm_charts/cgen-builder-webapp/dev.value.yaml b/deploy/release-deploy/helm_charts/cgen-builder-webapp/dev.value.yaml new file mode 100644 index 00000000..401b2d79 --- /dev/null +++ b/deploy/release-deploy/helm_charts/cgen-builder-webapp/dev.value.yaml @@ -0,0 +1,88 @@ +# Default values for cgen-builder-webapp. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: cgen-builder-webapp.o2bus.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: tls-secret-o2bus-cgen-builder-webapp + hosts: + - cgen-builder-webapp.o2bus.com +# env: +# values: +# - name: CHOKIDAR_USEPOLLING +# value: true +# - name: REACT_APP_NAME +# value: dockerEnv +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/release-deploy/helm_charts/cgen-builder-webapp/values.yaml b/deploy/release-deploy/helm_charts/cgen-builder-webapp/values.yaml index 401b2d79..98c8e22b 100644 --- a/deploy/release-deploy/helm_charts/cgen-builder-webapp/values.yaml +++ b/deploy/release-deploy/helm_charts/cgen-builder-webapp/values.yaml @@ -48,14 +48,14 @@ ingress: kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - host: cgen-builder-webapp.o2bus.com + - host: cgen-builder-webapp.o2nextgen.com paths: - path: / pathType: ImplementationSpecific tls: - - secretName: tls-secret-o2bus-cgen-builder-webapp + - secretName: tls-secret-o2nextgen-cgen-builder-webapp hosts: - - cgen-builder-webapp.o2bus.com + - cgen-builder-webapp.o2nextgen.com # env: # values: # - name: CHOKIDAR_USEPOLLING diff --git a/src/DesktopApps/O2.Business b/src/DesktopApps/O2.Business index 84a7fb91..9303c5f6 160000 --- a/src/DesktopApps/O2.Business +++ b/src/DesktopApps/O2.Business @@ -1 +1 @@ -Subproject commit 84a7fb9169fb0c959701876d723ba13fea5ad66c +Subproject commit 9303c5f63ae9c71f72c3d1a9b79bbaadd9eca62c diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/c-gen/CertificateViewModel.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/CGen/CertificateViewModel.cs similarity index 74% rename from src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/c-gen/CertificateViewModel.cs rename to src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/CGen/CertificateViewModel.cs index 04f7e12e..2036d972 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/c-gen/CertificateViewModel.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/CGen/CertificateViewModel.cs @@ -1,4 +1,4 @@ -namespace O2NextGen.Sdk.NetCore.Models.c_gen +namespace O2NextGen.Sdk.NetCore.Models.CGen { public class CertificateViewModel { diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/e-sender/EmailRequestViewModel.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/ESender/EmailRequestViewModel.cs similarity index 90% rename from src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/e-sender/EmailRequestViewModel.cs rename to src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/ESender/EmailRequestViewModel.cs index bab91a2c..3133ca4f 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/e-sender/EmailRequestViewModel.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/ESender/EmailRequestViewModel.cs @@ -1,4 +1,4 @@ -namespace O2NextGen.Sdk.NetCore.Models.e_sender +namespace O2NextGen.Sdk.NetCore.Models.ESender { public class EmailRequestViewModel : IViewModel { diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/e-sender/IViewModel.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/ESender/IViewModel.cs similarity index 84% rename from src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/e-sender/IViewModel.cs rename to src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/ESender/IViewModel.cs index f991eeca..6b30bfac 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/e-sender/IViewModel.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/ESender/IViewModel.cs @@ -1,4 +1,4 @@ -namespace O2NextGen.Sdk.NetCore.Models.e_sender +namespace O2NextGen.Sdk.NetCore.Models.ESender { public interface IViewModel { diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/O2NextGen.Sdk.NetCore.Models.csproj b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/O2NextGen.Sdk.NetCore.Models.csproj index 84a4445b..b0c49147 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/O2NextGen.Sdk.NetCore.Models.csproj +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/O2NextGen.Sdk.NetCore.Models.csproj @@ -1,11 +1,11 @@  - netcoreapp2.2 + net6.0 - + diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/media-basket/MediaViewModel.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/media-basket/MediaViewModel.cs index 98c83840..a62a261e 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/media-basket/MediaViewModel.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/media-basket/MediaViewModel.cs @@ -1,7 +1,7 @@ using System; using Microsoft.AspNetCore.Http; -namespace O2NextGen.Sdk.NetCore.Models.media_basket +namespace O2NextGen.Sdk.NetCore.Models.MediaBasket { public class MediaViewModel { diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatEvent.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatEvent.cs index 893fec09..bb9a91ac 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatEvent.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatEvent.cs @@ -1,4 +1,4 @@ -namespace O2NextGen.Sdk.NetCore.Models.smalltalk +namespace O2NextGen.Sdk.NetCore.Models.SmallTalk { public class ChatEvent { diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessage.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessage.cs index 5152b63e..ad6a4855 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessage.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessage.cs @@ -1,4 +1,4 @@ -namespace O2NextGen.Sdk.NetCore.Models.smalltalk +namespace O2NextGen.Sdk.NetCore.Models.SmallTalk { public class ChatMessage { diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessageViewModel.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessageViewModel.cs index 99e020e8..65ad7140 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessageViewModel.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatMessageViewModel.cs @@ -1,4 +1,4 @@ -namespace O2NextGen.Sdk.NetCore.Models.smalltalk +namespace O2NextGen.Sdk.NetCore.Models.SmallTalk { public class ChatMessageViewModel diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSession.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSession.cs index 330d14bd..8fafb68e 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSession.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSession.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace O2NextGen.Sdk.NetCore.Models.smalltalk +namespace O2NextGen.Sdk.NetCore.Models.SmallTalk { public class ChatSession diff --git a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSessionInvite.cs b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSessionInvite.cs index 26aabc5f..f1d959fe 100644 --- a/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSessionInvite.cs +++ b/src/SDKs/O2NG.Sdk.NetCore/O2NG.Sdk.NetCore.Models/smalltalk/ChatSessionInvite.cs @@ -1,4 +1,4 @@ -namespace O2NextGen.Sdk.NetCore.Models.smalltalk +namespace O2NextGen.Sdk.NetCore.Models.SmallTalk { public abstract class ChatSessionInvite { diff --git a/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/O2NextGen.Sdk.NetCore.Extensions.csproj b/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/O2NextGen.Sdk.NetCore.Extensions.csproj new file mode 100644 index 00000000..bafd05b1 --- /dev/null +++ b/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/O2NextGen.Sdk.NetCore.Extensions.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/O2NextGen.Sdk.NetCore.Extensions.sln b/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/O2NextGen.Sdk.NetCore.Extensions.sln new file mode 100644 index 00000000..32b79a81 --- /dev/null +++ b/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/O2NextGen.Sdk.NetCore.Extensions.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 25.0.1704.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "O2NextGen.Sdk.NetCore.Extensions", "O2NextGen.Sdk.NetCore.Extensions.csproj", "{73C6B6D2-413C-444C-A8D8-9C51A45669B2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {73C6B6D2-413C-444C-A8D8-9C51A45669B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73C6B6D2-413C-444C-A8D8-9C51A45669B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73C6B6D2-413C-444C-A8D8-9C51A45669B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73C6B6D2-413C-444C-A8D8-9C51A45669B2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {28AD8BAB-FD1C-46E6-9F11-6F4DA2FCC91E} + EndGlobalSection +EndGlobal diff --git a/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/UnixDateExtensions.cs b/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/UnixDateExtensions.cs new file mode 100644 index 00000000..3ad2b0f9 --- /dev/null +++ b/src/SDKs/O2NextGen.Sdk.NetCore.Extensions/UnixDateExtensions.cs @@ -0,0 +1,19 @@ +namespace O2NextGen.Sdk.NetCore.Extensions +{ + public static class UnixDateExtensions + { + private static readonly DateTime UnixEpoch = + new DateTime(1970, 1, 1, 0, 0, 0, 0).ToLocalTime(); + + public static DateTime ConvertToDateTime(this long seconds, bool isSeconds = true) + { + return isSeconds ? UnixEpoch.AddSeconds(seconds) : UnixEpoch.AddMinutes(seconds); + } + + public static long ConvertToUnixTime(this DateTime datetime, bool isSeconds = true) + { + return isSeconds ? (long)(datetime - UnixEpoch).TotalSeconds : (long)(datetime - UnixEpoch).TotalMinutes; + } + } +} + diff --git a/src/Services/auth/.dockerignore b/src/Services/auth/.dockerignore new file mode 100644 index 00000000..bdca33b4 --- /dev/null +++ b/src/Services/auth/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Services/auth/Dockerfile b/src/Services/auth/Dockerfile new file mode 100644 index 00000000..5621482e --- /dev/null +++ b/src/Services/auth/Dockerfile @@ -0,0 +1,21 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj", "O2NextGen.Auth.Web/"] +RUN dotnet restore "O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj" +COPY . . +WORKDIR "/src/O2NextGen.Auth.Web" +RUN dotnet build "O2NextGen.Auth.Web.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "O2NextGen.Auth.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "O2NextGen.Auth.Web.dll"] diff --git a/src/Services/auth/O2NextGen.Auth.Web/.dockerignore b/src/Services/auth/O2NextGen.Auth.Web/.dockerignore new file mode 100644 index 00000000..bdca33b4 --- /dev/null +++ b/src/Services/auth/O2NextGen.Auth.Web/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Services/auth/O2NextGen.Auth.Web/Dockerfile b/src/Services/auth/O2NextGen.Auth.Web/Dockerfile index 627b64fa..11e00173 100644 --- a/src/Services/auth/O2NextGen.Auth.Web/Dockerfile +++ b/src/Services/auth/O2NextGen.Auth.Web/Dockerfile @@ -1,17 +1,9 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -#FROM node as react - FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 -RUN apt-get update -RUN apt-get install -y curl -RUN apt-get install -y libpng-dev libjpeg-dev curl libxi6 build-essential libgl1-mesa-glx -RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - -RUN apt-get install -y nodejs - FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build RUN apt-get update RUN apt-get install -y curl @@ -20,10 +12,10 @@ RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - RUN apt-get install -y nodejs WORKDIR /src -COPY ["Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj", "Services/auth/O2NextGen.Auth.Web/"] -RUN dotnet restore "Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj" +COPY ["O2NextGen.Auth.Web.csproj", "."] +RUN dotnet restore "./O2NextGen.Auth.Web.csproj" COPY . . -WORKDIR "/src/Services/auth/O2NextGen.Auth.Web" +WORKDIR "/src/." RUN dotnet build "O2NextGen.Auth.Web.csproj" -c Release -o /app/build FROM build AS publish diff --git a/src/Services/auth/O2NextGen.Auth.Web/Extensions/IdentityServerExtensions.cs b/src/Services/auth/O2NextGen.Auth.Web/Extensions/IdentityServerExtensions.cs index 7a7bdaf0..2ffd8837 100644 --- a/src/Services/auth/O2NextGen.Auth.Web/Extensions/IdentityServerExtensions.cs +++ b/src/Services/auth/O2NextGen.Auth.Web/Extensions/IdentityServerExtensions.cs @@ -178,6 +178,43 @@ private static IEnumerable GetClients() AllowAccessTokensViaBrowser = true, RequireConsent = false, }, + new Client() + { + ClientId = "mvc", + ClientName = "MVC Client", + AllowedGrantTypes = GrantTypes.Implicit, + RequireClientSecret =false, + RedirectUris = { "https://localhost:5003/signin-oidc"}, + PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc"}, + AllowedCorsOrigins = { "https://localhost:5003" }, + AllowedScopes = new List() + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile + } + }, + new Client() + { + ClientId = "client.pfrcenter.blazor", + ClientName = "client.pfrcenter.blazor", + RequireClientSecret = false, + RequireConsent = false, + RequirePkce = true, + AllowedScopes = { + IdentityServerConstants.StandardScopes.Address, + IdentityServerConstants.StandardScopes.Email, + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile , + }, + RedirectUris = + { + "https://localhost:7001/authentication/login-callback " + }, + PostLogoutRedirectUris = + { + "https://localhost:7001/authentication/logout-callback " + } + } }; } diff --git a/src/Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj b/src/Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj index 41e8669e..edf4b1f0 100644 --- a/src/Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj +++ b/src/Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.csproj @@ -3,7 +3,7 @@ net6.0 OutOfProcess - ../../../docker-compose.dcproj + docker-compose.dcproj adca6dac-ab6b-4a1a-b8c4-03f600c16f45 Linux ..\..\.. diff --git a/src/Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.sln b/src/Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.sln new file mode 100644 index 00000000..36cb12f7 --- /dev/null +++ b/src/Services/auth/O2NextGen.Auth.Web/O2NextGen.Auth.Web.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 25.0.1703.8 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "O2NextGen.Auth.Web", "O2NextGen.Auth.Web.csproj", "{56C22C61-109A-4889-BDFB-A49867F253B4}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{84E5B702-0452-4CEE-BC71-00CC31863DE6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {56C22C61-109A-4889-BDFB-A49867F253B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56C22C61-109A-4889-BDFB-A49867F253B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56C22C61-109A-4889-BDFB-A49867F253B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56C22C61-109A-4889-BDFB-A49867F253B4}.Release|Any CPU.Build.0 = Release|Any CPU + {84E5B702-0452-4CEE-BC71-00CC31863DE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84E5B702-0452-4CEE-BC71-00CC31863DE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84E5B702-0452-4CEE-BC71-00CC31863DE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84E5B702-0452-4CEE-BC71-00CC31863DE6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D4678E18-F411-4500-8955-4A9E4DF31D00} + EndGlobalSection +EndGlobal diff --git a/src/Services/auth/O2NextGen.Auth.Web/appsettings.Development.json b/src/Services/auth/O2NextGen.Auth.Web/appsettings.Development.json index 0623a3f4..fc978751 100644 --- a/src/Services/auth/O2NextGen.Auth.Web/appsettings.Development.json +++ b/src/Services/auth/O2NextGen.Auth.Web/appsettings.Development.json @@ -5,5 +5,6 @@ "System": "Information", "Microsoft": "Information" } - } + }, + "ConnectionString": "Server=52.185.108.164;Initial Catalog=O2NextGen.AuthDb-Dev;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30;" } diff --git a/src/Services/auth/O2NextGen.Auth.Web/docker-compose.dcproj b/src/Services/auth/O2NextGen.Auth.Web/docker-compose.dcproj new file mode 100644 index 00000000..2b62a357 --- /dev/null +++ b/src/Services/auth/O2NextGen.Auth.Web/docker-compose.dcproj @@ -0,0 +1,18 @@ + + + + 2.1 + Linux + {84E5B702-0452-4CEE-BC71-00CC31863DE6} + True + {Scheme}://localhost:{ServicePort} + o2nextgen.auth.web + + + + docker-compose.yml + + + + + diff --git a/src/Services/auth/O2NextGen.Auth.Web/docker-compose.override.yml b/src/Services/auth/O2NextGen.Auth.Web/docker-compose.override.yml new file mode 100644 index 00000000..9ca78647 --- /dev/null +++ b/src/Services/auth/O2NextGen.Auth.Web/docker-compose.override.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + o2nextgen.auth.web: + environment: + - ASPNETCORE_ENVIRONMENT=Development + ports: + - "80" diff --git a/src/Services/auth/O2NextGen.Auth.Web/docker-compose.yml b/src/Services/auth/O2NextGen.Auth.Web/docker-compose.yml new file mode 100644 index 00000000..7ca79689 --- /dev/null +++ b/src/Services/auth/O2NextGen.Auth.Web/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + o2nextgen.auth.web: + image: ${DOCKER_REGISTRY-}o2nextgenauth + build: + context: . + dockerfile: ./Dockerfile diff --git a/src/Services/auth/O2NextGen.Auth.sln b/src/Services/auth/O2NextGen.Auth.sln index 96bd19f0..2fd5215a 100644 --- a/src/Services/auth/O2NextGen.Auth.sln +++ b/src/Services/auth/O2NextGen.Auth.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests.O2NextGen. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.O2NextGen.Auth.Web", "Tests\Tests.O2NextGen.Auth.Web\Tests.O2NextGen.Auth.Web.csproj", "{D0C86E83-9BAB-446C-906C-1AD166084612}" EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{CA0A6013-EFAC-48E0-BC2C-7E1D1CE4BD89}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +31,10 @@ Global {D0C86E83-9BAB-446C-906C-1AD166084612}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0C86E83-9BAB-446C-906C-1AD166084612}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0C86E83-9BAB-446C-906C-1AD166084612}.Release|Any CPU.Build.0 = Release|Any CPU + {CA0A6013-EFAC-48E0-BC2C-7E1D1CE4BD89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA0A6013-EFAC-48E0-BC2C-7E1D1CE4BD89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA0A6013-EFAC-48E0-BC2C-7E1D1CE4BD89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA0A6013-EFAC-48E0-BC2C-7E1D1CE4BD89}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Services/auth/docker-compose.dcproj b/src/Services/auth/docker-compose.dcproj new file mode 100644 index 00000000..0c7f1eaa --- /dev/null +++ b/src/Services/auth/docker-compose.dcproj @@ -0,0 +1,18 @@ + + + + 2.1 + Linux + {CA0A6013-EFAC-48E0-BC2C-7E1D1CE4BD89} + True + {Scheme}://localhost:{ServicePort} + o2nextgen.auth.web + + + + docker-compose.yml + + + + + diff --git a/src/Services/auth/docker-compose.override.yml b/src/Services/auth/docker-compose.override.yml new file mode 100644 index 00000000..9ca78647 --- /dev/null +++ b/src/Services/auth/docker-compose.override.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + o2nextgen.auth.web: + environment: + - ASPNETCORE_ENVIRONMENT=Development + ports: + - "80" diff --git a/src/Services/auth/docker-compose.yml b/src/Services/auth/docker-compose.yml new file mode 100644 index 00000000..a0787a77 --- /dev/null +++ b/src/Services/auth/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + o2nextgen.auth.web: + image: ${DOCKER_REGISTRY-}o2nextgenauth + build: + context: . + dockerfile: O2NextGen.Auth.Web/Dockerfile diff --git a/src/Services/c-gen/.config/dotnet-tools.json b/src/Services/c-gen/.config/dotnet-tools.json new file mode 100644 index 00000000..d1ac7eef --- /dev/null +++ b/src/Services/c-gen/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "0.21.0", + "commands": [ + "dotnet-csharpier" + ] + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/.vscode/launch.json b/src/Services/c-gen/.vscode/launch.json new file mode 100644 index 00000000..379ff20e --- /dev/null +++ b/src/Services/c-gen/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/O2NextGen.CertificateManagement.Application/bin/Debug/net6.0/O2NextGen.CertificateManagement.Application.dll", + "args": [], + "cwd": "${workspaceFolder}/O2NextGen.CertificateManagement.Application", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/src/Services/c-gen/.vscode/settings.json b/src/Services/c-gen/.vscode/settings.json new file mode 100644 index 00000000..37099cae --- /dev/null +++ b/src/Services/c-gen/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "dotnet-test-explorer.enableTelemetry": false, + "dotnet-test-explorer.testProjectPath": "Tests/*/*Tests.*.csproj" +} \ No newline at end of file diff --git a/src/Services/c-gen/.vscode/tasks.json b/src/Services/c-gen/.vscode/tasks.json new file mode 100644 index 00000000..0ccddd83 --- /dev/null +++ b/src/Services/c-gen/.vscode/tasks.json @@ -0,0 +1,72 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "test", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "${workspaceFolder}/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.csproj" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "test with coverage", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "/p:CollectCoverage=true", + "/p:CoverletOutputFormat=lcov", + "/p:CoverletOutput=./lcov.info", + "${workspaceFolder}/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.csproj" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "test", + "isDefault": true + } + }, + ] +} \ No newline at end of file diff --git a/src/Services/c-gen/Dockerfile b/src/Services/c-gen/Dockerfile new file mode 100644 index 00000000..4d8e2e66 --- /dev/null +++ b/src/Services/c-gen/Dockerfile @@ -0,0 +1,25 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj", "O2NextGen.CertificateManagement.Application/"] +COPY ["O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.csproj", "O2NextGen.CertificateManagement.Domain/"] +COPY ["O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.csproj", "O2NextGen.CertificateManagement.Infrastructure/"] +COPY ["O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.csproj", "O2NextGen.CertificateManagement.StartupTasks/"] +RUN dotnet restore "O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj" +COPY . . +WORKDIR "/src/O2NextGen.CertificateManagement.Application" +RUN dotnet build "O2NextGen.CertificateManagement.Application.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "O2NextGen.CertificateManagement.Application.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "O2NextGen.CertificateManagement.Application.dll"] diff --git a/src/Services/c-gen/O2NextGen.CGen.sln b/src/Services/c-gen/O2NextGen.CGen.sln index 04fc0c40..ffc04efb 100644 --- a/src/Services/c-gen/O2NextGen.CGen.sln +++ b/src/Services/c-gen/O2NextGen.CGen.sln @@ -13,15 +13,37 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{12E83C2A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "O2NextGen.CertificateManagement.Data", "O2NextGen.CertificateManagement.Data\O2NextGen.CertificateManagement.Data.csproj", "{B8C8A05D-0D30-40FF-8BDD-B2FBA2ED7AB0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests.O2NextGen.CertificateManagement.Api", "Tests\IntegrationTests.O2NextGen.CertificateManagement.Api\IntegrationTests.O2NextGen.CertificateManagement.Api.csproj", "{454CB1E8-8060-49CB-9752-140F93E7829C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.O2NextGen.CertificateManagement.Api", "Tests\Tests.O2NextGen.CertificateManagement.Api\Tests.O2NextGen.CertificateManagement.Api.csproj", "{CEA7184F-87D5-4141-A4BC-A983BF257AC6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.O2NextGen.CertificateManagement.Impl", "Tests\Tests.O2NextGen.CertificateManagement.Impl\Tests.O2NextGen.CertificateManagement.Impl.csproj", "{6FB50F98-47E7-4960-80F0-58562914ADDE}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDKs", "SDKs", "{629874CA-6688-43CA-9716-48FB42D41150}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests.O2NextGen.CertificateManagement.Api", "Tests\IntegrationTests.O2NextGen.CertificateManagement.Api\IntegrationTests.O2NextGen.CertificateManagement.Api.csproj", "{094FF4EB-0351-44BF-9739-4F3A62721DFC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDKs", "SDKs", "{54AADF48-C069-49F7-B8A8-3B72F5FC3F14}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "O2NextGen.Sdk.NetCore.Models", "..\..\SDKs\O2NG.Sdk.NetCore\O2NG.Sdk.NetCore.Models\O2NextGen.Sdk.NetCore.Models.csproj", "{AFA64E01-DA06-4432-9BEA-34D0F4AD6A54}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "O2NextGen.CertificateManagement.Domain", "O2NextGen.CertificateManagement.Domain\O2NextGen.CertificateManagement.Domain.csproj", "{30072535-9AFD-45CE-8258-87BF3CAA2EC2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "O2NextGen.CertificateManagement.StartupTasks", "O2NextGen.CertificateManagement.StartupTasks\O2NextGen.CertificateManagement.StartupTasks.csproj", "{9BE85DEE-293B-4E57-BD0F-01E3B48F7A61}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "O2NextGen.CertificateManagement.Infrastructure", "O2NextGen.CertificateManagement.Infrastructure\O2NextGen.CertificateManagement.Infrastructure.csproj", "{125F9B7B-1508-4F01-A9AA-2EBC01BE0C19}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CQRS", "CQRS", "{716B7603-CDF7-460D-A869-20FE90F84EC2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skeleton", "Skeleton", "{2561455C-AAF2-4B6F-BA84-DA8AD57EB3E4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "O2NextGen.CertificateManagement.Application", "O2NextGen.CertificateManagement.Application\O2NextGen.CertificateManagement.Application.csproj", "{BC6D2691-1006-4B6D-B661-81157E493F3B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests.O2NextGen.CertificateManagement.Application", "Tests\IntegrationTests.O2NextGen.CertificateManagement.Application\IntegrationTests.O2NextGen.CertificateManagement.Application.csproj", "{58CC0839-863D-4708-A6D4-42F245593538}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CQRS", "CQRS", "{0835E199-8062-45E0-899E-38ECAD9437CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.O2NextGen.CertificateManagement.Infrastructure", "Tests\Tests.O2NextGen.CertificateManagement.Infrastructure\Tests.O2NextGen.CertificateManagement.Infrastructure.csproj", "{EC5B79FF-4A87-4740-B5EC-2778861EB017}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.O2NextGen.CertificateManagement.Domain", "Tests\Tests.O2NextGen.CertificateManagement.Domain\Tests.O2NextGen.CertificateManagement.Domain.csproj", "{EFBD38BF-67DF-4507-94DC-E8BED67B6CF5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "O2NextGen.Sdk.NetCore.Models", "..\..\SDKs\O2NG.Sdk.NetCore\O2NG.Sdk.NetCore.Models\O2NextGen.Sdk.NetCore.Models.csproj", "{8C88AA87-9C8E-42D7-BAF1-28401C563FDF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.O2NextGen.CertificateManagement.Application", "Tests\Tests.O2NextGen.CertificateManagement.Application\Tests.O2NextGen.CertificateManagement.Application.csproj", "{6EE05F8B-1BF0-493A-9408-C824F5C2D271}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,10 +67,6 @@ Global {B8C8A05D-0D30-40FF-8BDD-B2FBA2ED7AB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8C8A05D-0D30-40FF-8BDD-B2FBA2ED7AB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8C8A05D-0D30-40FF-8BDD-B2FBA2ED7AB0}.Release|Any CPU.Build.0 = Release|Any CPU - {454CB1E8-8060-49CB-9752-140F93E7829C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {454CB1E8-8060-49CB-9752-140F93E7829C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {454CB1E8-8060-49CB-9752-140F93E7829C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {454CB1E8-8060-49CB-9752-140F93E7829C}.Release|Any CPU.Build.0 = Release|Any CPU {CEA7184F-87D5-4141-A4BC-A983BF257AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CEA7184F-87D5-4141-A4BC-A983BF257AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU {CEA7184F-87D5-4141-A4BC-A983BF257AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -57,19 +75,68 @@ Global {6FB50F98-47E7-4960-80F0-58562914ADDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6FB50F98-47E7-4960-80F0-58562914ADDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6FB50F98-47E7-4960-80F0-58562914ADDE}.Release|Any CPU.Build.0 = Release|Any CPU - {8C88AA87-9C8E-42D7-BAF1-28401C563FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8C88AA87-9C8E-42D7-BAF1-28401C563FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8C88AA87-9C8E-42D7-BAF1-28401C563FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C88AA87-9C8E-42D7-BAF1-28401C563FDF}.Release|Any CPU.Build.0 = Release|Any CPU + {094FF4EB-0351-44BF-9739-4F3A62721DFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {094FF4EB-0351-44BF-9739-4F3A62721DFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {094FF4EB-0351-44BF-9739-4F3A62721DFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {094FF4EB-0351-44BF-9739-4F3A62721DFC}.Release|Any CPU.Build.0 = Release|Any CPU + {AFA64E01-DA06-4432-9BEA-34D0F4AD6A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFA64E01-DA06-4432-9BEA-34D0F4AD6A54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFA64E01-DA06-4432-9BEA-34D0F4AD6A54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFA64E01-DA06-4432-9BEA-34D0F4AD6A54}.Release|Any CPU.Build.0 = Release|Any CPU + {30072535-9AFD-45CE-8258-87BF3CAA2EC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30072535-9AFD-45CE-8258-87BF3CAA2EC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30072535-9AFD-45CE-8258-87BF3CAA2EC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30072535-9AFD-45CE-8258-87BF3CAA2EC2}.Release|Any CPU.Build.0 = Release|Any CPU + {9BE85DEE-293B-4E57-BD0F-01E3B48F7A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BE85DEE-293B-4E57-BD0F-01E3B48F7A61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BE85DEE-293B-4E57-BD0F-01E3B48F7A61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BE85DEE-293B-4E57-BD0F-01E3B48F7A61}.Release|Any CPU.Build.0 = Release|Any CPU + {125F9B7B-1508-4F01-A9AA-2EBC01BE0C19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {125F9B7B-1508-4F01-A9AA-2EBC01BE0C19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {125F9B7B-1508-4F01-A9AA-2EBC01BE0C19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {125F9B7B-1508-4F01-A9AA-2EBC01BE0C19}.Release|Any CPU.Build.0 = Release|Any CPU + {BC6D2691-1006-4B6D-B661-81157E493F3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC6D2691-1006-4B6D-B661-81157E493F3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC6D2691-1006-4B6D-B661-81157E493F3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC6D2691-1006-4B6D-B661-81157E493F3B}.Release|Any CPU.Build.0 = Release|Any CPU + {58CC0839-863D-4708-A6D4-42F245593538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58CC0839-863D-4708-A6D4-42F245593538}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58CC0839-863D-4708-A6D4-42F245593538}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58CC0839-863D-4708-A6D4-42F245593538}.Release|Any CPU.Build.0 = Release|Any CPU + {EC5B79FF-4A87-4740-B5EC-2778861EB017}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC5B79FF-4A87-4740-B5EC-2778861EB017}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC5B79FF-4A87-4740-B5EC-2778861EB017}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC5B79FF-4A87-4740-B5EC-2778861EB017}.Release|Any CPU.Build.0 = Release|Any CPU + {EFBD38BF-67DF-4507-94DC-E8BED67B6CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFBD38BF-67DF-4507-94DC-E8BED67B6CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFBD38BF-67DF-4507-94DC-E8BED67B6CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFBD38BF-67DF-4507-94DC-E8BED67B6CF5}.Release|Any CPU.Build.0 = Release|Any CPU + {6EE05F8B-1BF0-493A-9408-C824F5C2D271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EE05F8B-1BF0-493A-9408-C824F5C2D271}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EE05F8B-1BF0-493A-9408-C824F5C2D271}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EE05F8B-1BF0-493A-9408-C824F5C2D271}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {454CB1E8-8060-49CB-9752-140F93E7829C} = {12E83C2A-23C7-4E71-B137-AC56CE28F9E4} + {EA540B06-8A73-49C7-A799-49423B316458} = {2561455C-AAF2-4B6F-BA84-DA8AD57EB3E4} + {C6E0B2A5-6349-4C1D-8E14-82249AF8F58A} = {2561455C-AAF2-4B6F-BA84-DA8AD57EB3E4} + {01805439-2954-49CE-93D2-5EB06B82146C} = {2561455C-AAF2-4B6F-BA84-DA8AD57EB3E4} + {B8C8A05D-0D30-40FF-8BDD-B2FBA2ED7AB0} = {2561455C-AAF2-4B6F-BA84-DA8AD57EB3E4} {CEA7184F-87D5-4141-A4BC-A983BF257AC6} = {12E83C2A-23C7-4E71-B137-AC56CE28F9E4} {6FB50F98-47E7-4960-80F0-58562914ADDE} = {12E83C2A-23C7-4E71-B137-AC56CE28F9E4} - {8C88AA87-9C8E-42D7-BAF1-28401C563FDF} = {629874CA-6688-43CA-9716-48FB42D41150} + {094FF4EB-0351-44BF-9739-4F3A62721DFC} = {12E83C2A-23C7-4E71-B137-AC56CE28F9E4} + {AFA64E01-DA06-4432-9BEA-34D0F4AD6A54} = {54AADF48-C069-49F7-B8A8-3B72F5FC3F14} + {30072535-9AFD-45CE-8258-87BF3CAA2EC2} = {716B7603-CDF7-460D-A869-20FE90F84EC2} + {9BE85DEE-293B-4E57-BD0F-01E3B48F7A61} = {716B7603-CDF7-460D-A869-20FE90F84EC2} + {125F9B7B-1508-4F01-A9AA-2EBC01BE0C19} = {716B7603-CDF7-460D-A869-20FE90F84EC2} + {BC6D2691-1006-4B6D-B661-81157E493F3B} = {716B7603-CDF7-460D-A869-20FE90F84EC2} + {58CC0839-863D-4708-A6D4-42F245593538} = {0835E199-8062-45E0-899E-38ECAD9437CD} + {0835E199-8062-45E0-899E-38ECAD9437CD} = {12E83C2A-23C7-4E71-B137-AC56CE28F9E4} + {EC5B79FF-4A87-4740-B5EC-2778861EB017} = {0835E199-8062-45E0-899E-38ECAD9437CD} + {EFBD38BF-67DF-4507-94DC-E8BED67B6CF5} = {0835E199-8062-45E0-899E-38ECAD9437CD} + {6EE05F8B-1BF0-493A-9408-C824F5C2D271} = {0835E199-8062-45E0-899E-38ECAD9437CD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A6DFC9F0-3FA7-492B-ACE9-3EFBF3477712} diff --git a/src/Services/c-gen/O2NextGen.CGen.v3.ncrunchsolution b/src/Services/c-gen/O2NextGen.CGen.v3.ncrunchsolution new file mode 100644 index 00000000..63251f99 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CGen.v3.ncrunchsolution @@ -0,0 +1,7 @@ + + + True + + True + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/CategoryController.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/CategoryController.cs new file mode 100644 index 00000000..d78835a9 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/CategoryController.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Mvc; +using O2NextGen.CertificateManagement.Api.Mappings; +using O2NextGen.CertificateManagement.Api.Setup; +using O2NextGen.CertificateManagement.Api.ViewModels; +using O2NextGen.CertificateManagement.Business.Services; +using System.Threading; +using System.Threading.Tasks; + +namespace O2NextGen.CertificateManagement.Api.Controllers +{ + [Route("category")] + public class CategoryController : ControllerBase + { + #region Fields + + private readonly ICategoryService _categoryService; + private readonly UrlsConfig _config; + + #endregion + + + #region Ctors + + public CategoryController(ICategoryService categoryService, UrlsConfig config) + { + _categoryService = categoryService; + _config = config; + } + + #endregion + + + #region Methods + + [HttpGet] + [Route("")] + public async Task GetAllAsync() + { + var url = _config.Auth; + var models = await _categoryService.GetAllAsync(CancellationToken.None); + return Ok(models.ToViewModel()); + } + + [HttpGet] + [Route("{id}")] + public async Task GetByIdAsync(long id, CancellationToken ct) + { + var certificate = await _categoryService.GetByIdAsync(id, ct); + if (certificate == null) + return NotFound(); + return Ok(certificate.ToViewModel()); + } + + [HttpPut] + [Route("id")] + public async Task UpdateAsync(long id, CategoryViewModel model, CancellationToken ct) + { + var certificate = await _categoryService.UpdateAsync(model.ToModel(), ct); + return Ok(certificate.ToViewModel()); + } + + [HttpPost] + [HttpPut] + [Route("")] + public async Task AddAsync(CategoryViewModel model, CancellationToken ct) + { + var certificate = await _categoryService.AddAsync(model.ToModel(), ct); + return CreatedAtAction(nameof(GetByIdAsync), new { id = certificate.Id }, certificate); + } + + [HttpDelete] + [Route("id")] + public async Task RemoveAsync(long id, CancellationToken ct) + { + await _categoryService.RemoveAsync(id, ct); + return NoContent(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/CertificatesController.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/CertificatesController.cs index aedcba5b..ae0b2b68 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/CertificatesController.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/CertificatesController.cs @@ -1,14 +1,14 @@ -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using O2NextGen.CertificateManagement.Api.Mappings; using O2NextGen.CertificateManagement.Api.Setup; using O2NextGen.CertificateManagement.Business.Services; -using O2NextGen.CertificateManagement.Api.Mappings; using O2NextGen.Sdk.NetCore.Models.c_gen; +using System.Threading; +using System.Threading.Tasks; namespace O2NextGen.CertificateManagement.Api.Controllers { - [Route("certificates")] + [Route("api/[controller]")] public class CertificatesController : ControllerBase { #region Fields @@ -65,17 +65,17 @@ public async Task UpdateAsync(long id, CertificateViewModel model public async Task AddAsync(CertificateViewModel model, CancellationToken ct) { var certificate = await _certificatesService.AddAsync(model.ToModel(), ct); - return CreatedAtAction(nameof(GetByIdAsync), new {id = certificate.Id}, certificate); + return CreatedAtAction(nameof(GetByIdAsync), new { id = certificate.Id }, certificate); } - #endregion - [HttpDelete] [Route("id")] - public async Task RemoveAsync(long id,CancellationToken ct) + public async Task RemoveAsync(long id, CancellationToken ct) { await _certificatesService.RemoveAsync(id, ct); return NoContent(); } + + #endregion } } \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/VersionController.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/VersionController.cs index 8034c294..5b16a180 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/VersionController.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Controllers/VersionController.cs @@ -1,26 +1,26 @@ -using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using System.Reflection; namespace O2NextGen.CertificateManagement.Api.Controllers { [AllowAnonymous] - public class VersionController:ControllerBase + public class VersionController : ControllerBase { #region Fields - private readonly IHostingEnvironment _environment; + private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; #endregion - + #region Ctors - public VersionController(IHostingEnvironment environment, ILogger logger) + public VersionController(IWebHostEnvironment environment, ILogger logger) { _environment = environment; _logger = logger; diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Dockerfile b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Dockerfile new file mode 100644 index 00000000..693a0c39 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Dockerfile @@ -0,0 +1,24 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.csproj", "O2NextGen.CertificateManagement.Api/"] +COPY ["O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.csproj", "O2NextGen.CertificateManagement.Business/"] +COPY ["O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.csproj", "O2NextGen.CertificateManagement.Data/"] +COPY ["O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.csproj", "O2NextGen.CertificateManagement.Impl/"] +RUN dotnet restore "O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.csproj" +COPY . . +WORKDIR "/src/O2NextGen.CertificateManagement.Api" +RUN dotnet build "O2NextGen.CertificateManagement.Api.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "O2NextGen.CertificateManagement.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "O2NextGen.CertificateManagement.Api.dll"] diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Filters/ApiExceptionFilter.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Filters/ApiExceptionFilter.cs index 34aecb64..d1aa33e3 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Filters/ApiExceptionFilter.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Filters/ApiExceptionFilter.cs @@ -4,7 +4,7 @@ namespace O2NextGen.CertificateManagement.Api.Filters { - public class ApiExceptionFilter: IExceptionFilter + public class ApiExceptionFilter : IExceptionFilter { public void OnException(ExceptionContext context) @@ -12,7 +12,7 @@ public void OnException(ExceptionContext context) if (context.ExceptionHandled is DbUpdateConcurrencyException) { context.Result = - new ConflictObjectResult(new {Message = "Entity was updated, please refresh your copy."}); + new ConflictObjectResult(new { Message = "Entity was updated, please refresh your copy." }); } } } diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/IoC/ServiceCollectionExtensions.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/IoC/ServiceCollectionExtensions.cs index f20c0057..da71868b 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/IoC/ServiceCollectionExtensions.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/IoC/ServiceCollectionExtensions.cs @@ -1,12 +1,13 @@ -using System; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; using O2NextGen.CertificateManagement.Api.Filters; using O2NextGen.CertificateManagement.Business.Services; using O2NextGen.CertificateManagement.Data; using O2NextGen.CertificateManagement.Impl.Services; +using System; namespace O2NextGen.CertificateManagement.Api.IoC { @@ -16,10 +17,10 @@ public static class ServiceCollectionExtensions public static TConfig ConfigurePOCO(this IServiceCollection services, IConfiguration configuration) where TConfig : class, new() { - if (services == null) - throw new ArgumentNullException(nameof(services)); - - if (configuration == null) + if (services == null) + throw new ArgumentNullException(nameof(services)); + + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); var config = new TConfig(); @@ -32,10 +33,19 @@ public static IServiceCollection AddRequiredMvcComponents(this IServiceCollectio { services.AddTransient(); - var mvcBuilder = services.AddMvcCore(options => { options.Filters.Add(); }); + var mvcBuilder = services + .AddMvcCore(options => + { + options.Filters.Add(); + }); + mvcBuilder.AddApiExplorer(); //for swagger - mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_2_2); - mvcBuilder.AddJsonFormatters(); + //mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_3_0); + mvcBuilder.AddNewtonsoftJson(o => + { + o.SerializerSettings.Converters.Add(new StringEnumConverter()); + o.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); return services; } diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Logging/ElasticJsonFormatter.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Logging/ElasticJsonFormatter.cs index 1cbb943d..669cdaf6 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Logging/ElasticJsonFormatter.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Logging/ElasticJsonFormatter.cs @@ -1,9 +1,9 @@ -using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Json; +using System.IO; namespace O2NextGen.CertificateManagement.Api.Logging { diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Mappings/CategoryMappings.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Mappings/CategoryMappings.cs new file mode 100644 index 00000000..67760f58 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Mappings/CategoryMappings.cs @@ -0,0 +1,58 @@ +using O2NextGen.CertificateManagement.Api.ViewModels; +using O2NextGen.CertificateManagement.Business.Models; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace O2NextGen.CertificateManagement.Api.Mappings +{ + public static class CategoryMappinsg + { + public static CategoryViewModel ToViewModel(this Category model) + { + if (model == null) + return null; + + var viewModel = new CategoryViewModel(); + + //Bindings + viewModel.Id = model.Id; + viewModel.Name = model.Name; + + return viewModel; + } + + public static Category ToModel(this CategoryViewModel viewModel) + { + if (viewModel == null) + return null; + + var model = new Category(); + + //Bindings + model.Id = viewModel.Id; + model.Name = viewModel.Name; + + return model; + } + + public static IReadOnlyCollection ToViewModel( + this IReadOnlyCollection models) + { + if (models.Count == 0) + { + return Array.Empty(); + } + + var subscription = new CategoryViewModel[models.Count]; + var i = 0; + foreach (var model in models) + { + subscription[i] = ToViewModel(model); + ++i; + } + + return new ReadOnlyCollection(subscription); + } + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Mappings/CertificateMappings.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Mappings/CertificateMappings.cs index 3955d335..6d4dad0a 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Mappings/CertificateMappings.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Mappings/CertificateMappings.cs @@ -1,8 +1,8 @@ +using O2NextGen.CertificateManagement.Business.Models; +using O2NextGen.Sdk.NetCore.Models.c_gen; using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using O2NextGen.CertificateManagement.Business.Models; -using O2NextGen.Sdk.NetCore.Models.c_gen; namespace O2NextGen.CertificateManagement.Api.Mappings { diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.csproj index ba989a7c..3f2a4725 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.csproj +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.csproj @@ -1,42 +1,44 @@  - netcoreapp2.2 + net6.0 e238f36a-3514-4436-b624-9b4f799bb82d Linux ..\..\.. - ..\..\..\docker-compose.dcproj + ../docker-compose.dcproj + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + - + + + @@ -56,9 +58,17 @@ + + + + + + + + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/O2NextGen.CertificateManagement.Api.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Program.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Program.cs index 6294804b..7b53c438 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Program.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Program.cs @@ -1,8 +1,8 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Serilog; +using System; +using System.Threading.Tasks; namespace O2NextGen.CertificateManagement.Api { @@ -11,14 +11,14 @@ public class Program public static readonly string Namespace = typeof(Program).Namespace; public static readonly string AppName = - Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); - + Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); + public static async Task Main(string[] args) - { - + { + try { - var host = CreateWebHostBuilder(args).Build(); + var host = CreateHostBuilder(args); Log.Information($"############### {AppName} ##############"); Log.Information("################# Starting Application #################"); await host.EnsureDbUpdate(); @@ -37,12 +37,13 @@ public static async Task Main(string[] args) } } - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + public static IWebHost CreateHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseSerilog((context, configuration) => { configuration.ReadFrom.Configuration(context.Configuration); }) - .UseStartup(); // <- Add this line + .UseStartup() + .Build(); } } diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Properties/launchSettings.json b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Properties/launchSettings.json index 18c3ea4c..df128c43 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Properties/launchSettings.json +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Properties/launchSettings.json @@ -1,20 +1,12 @@ { "profiles": { - "O2NextGen.CertificateManagement.Web": { + "O2NextGen.CertificateManagement.Api": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "applicationUrl": "http://localhost:39903", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:10002;http://localhost:5002" - }, - "Docker": { - "commandName": "Docker", - "launchBrowser": true, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", - "publishAllPorts": true, - "useSSL": true + } } } } \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Services/CustomerService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Services/CustomerService.cs new file mode 100644 index 00000000..fccb8e57 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Services/CustomerService.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using System; + +namespace O2NextGen.CertificateManagement.Api.Services +{ + public class CustomerService : ICustomerService + { + // ReSharper disable once NotAccessedField.Local + private IHttpContextAccessor _context; + + public CustomerService(IHttpContextAccessor context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public Guid CustomerId { get; } = Guid.Parse("A6D86517-CF70-4EEF-8340-BCBAA4B60C4A"); + public string CustomerDescription { get; set; } = "#PF_R Community"; + public string RegisterLink { get; set; } = "https://pfr-centr.com"; + public string AccountLink { get; set; } = "https://pfr-centr.com"; + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Services/ICustomerService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Services/ICustomerService.cs new file mode 100644 index 00000000..71cd9379 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Services/ICustomerService.cs @@ -0,0 +1,12 @@ +using System; + +namespace O2NextGen.CertificateManagement.Api.Services +{ + public interface ICustomerService + { + Guid CustomerId { get; } + string CustomerDescription { get; set; } + string RegisterLink { get; set; } + string AccountLink { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Setup/BasicConfiguration.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Setup/BasicConfiguration.cs index b35d28e0..985a09f4 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Setup/BasicConfiguration.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Setup/BasicConfiguration.cs @@ -2,6 +2,6 @@ namespace O2NextGen.CertificateManagement.Api.Setup { public class UrlsConfig { - public string Auth { get; set; } + public string Auth { get; set; } } } \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Startup.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Startup.cs index 91a4200e..0da04bad 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Startup.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/Startup.cs @@ -1,50 +1,52 @@ -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using O2NextGen.CertificateManagement.Api.Setup; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; using O2NextGen.CertificateManagement.Api.Helpers; using O2NextGen.CertificateManagement.Api.IoC; -using Swashbuckle.AspNetCore.Swagger; +using O2NextGen.CertificateManagement.Api.Setup; +using System.Net; +using System.Threading.Tasks; [assembly: ApiController] namespace O2NextGen.CertificateManagement.Api { public class Startup { - public IHostingEnvironment HostingEnvironment { get; private set; } + public IWebHostEnvironment HostingEnvironment { get; private set; } public IConfiguration AppConfiguration { get; private set; } - public Startup(IConfiguration appConfiguration, IHostingEnvironment env) + public Startup(IConfiguration appConfiguration, IWebHostEnvironment env) { this.HostingEnvironment = env; this.AppConfiguration = appConfiguration; } + public void ConfigureServices(IServiceCollection services) { services.AddRequiredMvcComponents(); services.AddBusiness(); services.AddSwaggerGen(options => { - options.DescribeAllEnumsAsStrings(); - options.SwaggerDoc("v1",new Info() + options.DescribeAllParametersInCamelCase(); + options.SwaggerDoc("v1", new OpenApiInfo() { Title = "O2NextGen Platform. C-Gen HTTP API", Version = "v1", Description = "C-Gen API Service. The service allows you to create certificates", - TermsOfService = "Terms of Service" + //TermsOfService = "Terms of Service" }); }); services.AddConfigEf(AppConfiguration); services.ConfigurePOCO(AppConfiguration.GetSection("Urls")); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -56,7 +58,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) { builder.Run(async context => { - context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; var error = context.Features.Get(); @@ -67,8 +69,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) } }); }); - } - + } + app.UseStaticFiles(); app.UseSwagger() .UseSwaggerUI(c => @@ -86,7 +88,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) await next.Invoke(); }); - app.UseMvc(); + app.UseRouting(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } } \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/StartupHelpers/DatabaseExtensions.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/StartupHelpers/DatabaseExtensions.cs index 83f7c4d2..80fb8edb 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/StartupHelpers/DatabaseExtensions.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/StartupHelpers/DatabaseExtensions.cs @@ -1,7 +1,7 @@ -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using O2NextGen.CertificateManagement.Data; +using System.Threading.Tasks; // ReSharper disable once CheckNamespace namespace Microsoft.AspNetCore.Hosting diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/ViewModels/CategoryViewModel.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/ViewModels/CategoryViewModel.cs new file mode 100644 index 00000000..aceabcbe --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/ViewModels/CategoryViewModel.cs @@ -0,0 +1,9 @@ +namespace O2NextGen.CertificateManagement.Api.ViewModels +{ + public class CategoryViewModel + { + public long Id { get; internal set; } + public string Name { get; internal set; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/appsettings.json b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/appsettings.json index 3134b830..cc0c3503 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Api/appsettings.json +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Api/appsettings.json @@ -1,5 +1,5 @@ { - "ConnectionString": "Server=localhost;Initial Catalog=O2NextGen.CertificateDb;Persist Security Info=False;User ID=sa;Password=your@Password;Connection Timeout=30;", + "ConnectionString": "Server=20.112.192.201;Initial Catalog=O2NextGen.CertificateDb;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30;", "Urls": { "Auth": "http://localhost:10001" }, @@ -29,5 +29,4 @@ "Microsoft.Hosting.Lifetime": "Information" } } -} } \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Controllers/VersionController.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Controllers/VersionController.cs new file mode 100644 index 00000000..2b79c796 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Controllers/VersionController.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace O2NextGen.CertificateManagement.Application.Controllers +{ + + [AllowAnonymous] + public class VersionController : ControllerBase + { + #region Fields + + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + #endregion + + + #region Ctors + + public VersionController(IWebHostEnvironment environment, ILogger logger) + { + _environment = environment; + _logger = logger; + } + + #endregion + + [HttpGet("[controller]")] + public object Index() + { + var exVersion = Assembly.GetExecutingAssembly().GetName().Version; + _logger.LogInformation($"get version - {exVersion}"); + return new + { + Environment = _environment.EnvironmentName, + Version = exVersion.ToString() + }; + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Dockerfile b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Dockerfile new file mode 100644 index 00000000..4d8e2e66 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Dockerfile @@ -0,0 +1,25 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj", "O2NextGen.CertificateManagement.Application/"] +COPY ["O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.csproj", "O2NextGen.CertificateManagement.Domain/"] +COPY ["O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.csproj", "O2NextGen.CertificateManagement.Infrastructure/"] +COPY ["O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.csproj", "O2NextGen.CertificateManagement.StartupTasks/"] +RUN dotnet restore "O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj" +COPY . . +WORKDIR "/src/O2NextGen.CertificateManagement.Application" +RUN dotnet build "O2NextGen.CertificateManagement.Application.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "O2NextGen.CertificateManagement.Application.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "O2NextGen.CertificateManagement.Application.dll"] diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/CategoriesController.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/CategoriesController.cs new file mode 100644 index 00000000..4259dabf --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/CategoriesController.cs @@ -0,0 +1,116 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.CreateCategory; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.DeleteCategory; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategories; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategory; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.UpdateCategory; + +namespace O2NextGen.CertificateManagement.Application.Features.Categories +{ + [Route("api/[controller]")] + //[ApiVersion("1.0")] + [ApiController] + public class CategoriesController : ControllerBase + { + #region Fields + + private readonly IMediator _mediator; + private readonly ILogger _logger; + + private static readonly string GetByIdActionName + = nameof(GetByIdAsync).Replace("Async", string.Empty); + + #endregion + + + #region Ctors + + public CategoriesController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + #endregion + + + #region Methods + + [HttpGet] + [Route("{id}")] + public async Task GetByIdAsync(long id, CancellationToken ct) + { + _logger.LogInformation("Call API method {ByIdAsyncName}: id = {Id}", nameof(GetByIdAsync), id); + + var result = await _mediator.Send(new GetCategoryQuery(id), ct); + + if (result is null) + { + _logger.LogError("GetByIdAsync: not found id = {Id}", id); + return NotFound(); + } + + _logger.LogInformation("GetByIdAsync: OK"); + return Ok(result); + } + + [HttpGet] + [Route("")] + public async Task GetAllAsync() + { + var result = await _mediator.Send(new GetCategoriesQuery()); + return Ok(result.Categories); + } + + [HttpPut] + [HttpPost] + [Route("")] + public async Task> AddAsync( + [FromBody] CreateCategoryModel model, + CancellationToken ct) + { + var result = await _mediator.Send( + new CreateCategoryCommand( + model.CategoryName, + model.QuantityCertificates, + model.CategoryDescription, + model.CategorySeries), + ct); + return CreatedAtAction(GetByIdActionName, + new {id = result.Id}, result); + } + + [HttpPut] + [Route("{id}")] + public async Task> UpdateAsync( + long id, [FromBody] UpdateCategoryModel model, CancellationToken ct) + { + var result = await _mediator.Send( + new UpdateCategoryDetailsCommand( + model.Id, + model.CategoryName, + model.CategoryDescription, + model.CategorySeries, + model.CustomerId, + model.QuantityCertificates, + model.QuantityPublishCode), ct); + + if (result is null) + { + return NotFound(); + } + + return result; + } + + [HttpDelete] + [Route("{id}")] + public async Task RemoveAsync(long id, CancellationToken ct) + { + await _mediator.Send(new DeleteCategoryCommand(id), ct); + return NoContent(); + } + #endregion + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/CreateCategoryModel.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/CreateCategoryModel.cs new file mode 100644 index 00000000..636ca9bc --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/CreateCategoryModel.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace O2NextGen.CertificateManagement.Application.Features.Categories +{ + public class CreateCategoryModel : ICreateCategoryModel + { + public string CategoryName { get; set; } + public string CategoryDescription { get; set; } + public string CategorySeries { get; set; } + public string CustomerId { get; set; } + public long? AddedDate { get; set; } + public long? ModifiedDate { get; set; } + public long? DeletedDate { get; set; } + public bool? IsDeleted { get; set; } + public int TimeLifeInDays { get; set; } + public int QuantityCertificates { get; set; } + public int QuantityPublishCode { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/ICreateCategoryModel.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/ICreateCategoryModel.cs new file mode 100644 index 00000000..1329e634 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/ICreateCategoryModel.cs @@ -0,0 +1,11 @@ +namespace O2NextGen.CertificateManagement.Application.Features.Categories; + +public interface ICreateCategoryModel +{ + string CategoryName { get; set; } + string CategoryDescription { get; set; } + string CategorySeries { get; set; } + string CustomerId { get; set; } + public int QuantityCertificates { get; set; } + public int QuantityPublishCode { get; set; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/ResponseDto.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/ResponseDto.cs new file mode 100644 index 00000000..2f2daeb2 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/ResponseDto.cs @@ -0,0 +1,6 @@ +namespace O2NextGen.CertificateManagement.Application.Features.Categories; + +public class ResponseDto +{ + +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/UpdateCategoryModel.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/UpdateCategoryModel.cs new file mode 100644 index 00000000..f86f7efb --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Categories/UpdateCategoryModel.cs @@ -0,0 +1,6 @@ +namespace O2NextGen.CertificateManagement.Application.Features.Categories; + +public class UpdateCategoryModel : CreateCategoryModel +{ + public long Id { get; set; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/CertificatesController.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/CertificatesController.cs new file mode 100644 index 00000000..f5410efa --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/CertificatesController.cs @@ -0,0 +1,132 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.CreateCertificate; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.DeleteCertificate; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificate; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificates; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.UpdateCertificate; + +namespace O2NextGen.CertificateManagement.Application.Features.Certificates +{ + [Route("api/[controller]")] + [ApiController] + public partial class CertificatesController : ControllerBase + { + #region Fields + + private readonly IMediator _mediator; + private readonly ILogger _logger; + + private static readonly string GetByIdActionName + = nameof(GetByIdAsync).Replace("Async", string.Empty); + #endregion + + + #region Ctors + + public CertificatesController(IMediator mediator, ILogger logger) + { + _mediator = mediator; + _logger = logger; + } + + #endregion + + + #region Methods + + [HttpGet] + [Route("{id}")] + public async Task GetByIdAsync(long id, CancellationToken ct) + { + var result = await _mediator.Send(new GetCertificateQuery(id), ct); + + if (result is null) + return NotFound(); + + return Ok(result); + } + + [HttpGet] + [Route("")] + public async Task GetAllAsync() + { + var result = await _mediator.Send(new GetCertificatesQuery()); + return Ok(result.Certificates); + } + + [HttpPut] + [Route("id")] + public async Task> UpdateAsync( + long id, [FromBody] UpdateCertificateDetailsCommandModel model, CancellationToken ct) + { + var result = await _mediator.Send( + new UpdateCertificateDetailsCommand( + id, + model.ExternalId, + model.ModifiedDate, + model.AddedDate, + model.DeletedDate, + model.IsDeleted, + model.OwnerAccountId, + model.CustomerId, + model.ExpiredDate, + model.PublishDate, + model.CreatorId, + model.PublishCode, + model.IsVisible, + model.CategoryId, + model.Category, + model.Lock, + model.LockedDate, + model.LockInfo, + model.LanguageInfos)); + + if (result is null) + { + return NotFound(); + } + + return result; + } + + [HttpPut] + [HttpPost] + [Route("")] + public async Task> AddAsync( + [FromBody] CreateCertificateDetailsCommandModel model, + CancellationToken ct) + { + var result = await _mediator.Send( + new CreateCertificateCommand( + model.ExternalId, + model.IsDeleted, + model.OwnerAccountId, + model.CustomerId, + model.ExpiredDate, + model.PublishDate, + model.CreatorId, + model.PublishCode, + model.IsVisible, + model.CategoryId, + model.Category, + model.Lock, + model.LockedDate, + model.LockInfo, + model.LanguageInfos + )); + return CreatedAtAction(GetByIdActionName, + new { id = result.Id }, result); + } + + [HttpDelete] + [Route("{id}")] + public async Task RemoveAsync(long id, CancellationToken ct) + { + await _mediator.Send(new DeleteCertificateCommand(id)); + return NoContent(); + } + + #endregion + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/CreateCertificateDetailsCommandModel.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/CreateCertificateDetailsCommandModel.cs new file mode 100644 index 00000000..e435afd5 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/CreateCertificateDetailsCommandModel.cs @@ -0,0 +1,24 @@ +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Application.Features.Certificates; + +public class CreateCertificateDetailsCommandModel +{ + public string ExternalId { get; set; } + public bool? IsDeleted { get; set; } + + public string OwnerAccountId { get; set; } + public string CustomerId { get; set; } + public long ExpiredDate { get; set; } + public long PublishDate { get; set; } + public string CreatorId { get; set; } + public string PublishCode { get; set; } + public bool IsVisible { get; set; } + + public long CategoryId { get; set; } + public Category Category { get; set; } + public bool Lock { get; set; } + public long LockedDate { get; set; } + public string LockInfo { get; set; } + public ICollection LanguageInfos { get; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/UpdateCertificateDetailsCommandModel.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/UpdateCertificateDetailsCommandModel.cs new file mode 100644 index 00000000..ea5d7057 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Certificates/UpdateCertificateDetailsCommandModel.cs @@ -0,0 +1,27 @@ +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Application.Features.Certificates; + +public class UpdateCertificateDetailsCommandModel +{ + public string ExternalId { get; set; } + public bool? IsDeleted { get; set; } + + public string OwnerAccountId { get; set; } + public string CustomerId { get; set; } + public long ExpiredDate { get; set; } + public long PublishDate { get; set; } + public string CreatorId { get; set; } + public string PublishCode { get; set; } + public bool IsVisible { get; set; } + + public long CategoryId { get; set; } + public Category Category { get; set; } + public bool Lock { get; set; } + public long LockedDate { get; set; } + public string LockInfo { get; set; } + public ICollection LanguageInfos { get; } + public bool? ModifiedDate { get; internal set; } + public string AddedDate { get; internal set; } + public string DeletedDate { get; internal set; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Projects/ProjectViewModel.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Projects/ProjectViewModel.cs new file mode 100644 index 00000000..990a10f8 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Projects/ProjectViewModel.cs @@ -0,0 +1,8 @@ +namespace O2NextGen.CertificateManagement.Application.Features.Projects; + +public class ProjectViewModel +{ + public string Name { get; set; } + public string Description { get; set; } + public Guid TenantId { get; set; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Projects/ProjectsController.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Projects/ProjectsController.cs new file mode 100644 index 00000000..934e41eb --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Features/Projects/ProjectsController.cs @@ -0,0 +1,82 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc; +using O2NextGen.CertificateManagement.Application.Services; + +namespace O2NextGen.CertificateManagement.Application.Features.Projects; + +[Route("api/[controller]")] +public class ProjectsController : ControllerBase +{ + private readonly ICustomerService _customerService; + + private List memoryList { get; } + + public ProjectsController(ICustomerService customerService) + { + _customerService = customerService; + memoryList = new List + { + new ProjectViewModel + { + Name = "PROJECT PFR", + Description = "Project of PFR CENTER", + TenantId = _customerService.CustomerId + }, + new ProjectViewModel() + { + Name = "PROJECT PFR 2", + Description = "Project of PFR CENTER", + TenantId = _customerService.CustomerId + } + }; + } + + [HttpGet] + [ProducesResponseType((int) HttpStatusCode.NotFound)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + [ProducesResponseType((int) HttpStatusCode.Accepted)] + [Route("{id:long}")] + public IActionResult GetById() + { + return Ok(memoryList.FirstOrDefault()); + } + + [HttpGet] + [Route("")] + [ProducesResponseType((int) HttpStatusCode.NotFound)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + [ProducesResponseType((int) HttpStatusCode.Accepted)] + public async Task GetAllAsync() + { + return Ok(memoryList); + } + + [HttpPut] + [Route("id")] + [ProducesResponseType((int) HttpStatusCode.NotFound)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + [ProducesResponseType((int) HttpStatusCode.Accepted)] + public async Task UpdateAsync() + { + return Ok(memoryList.FirstOrDefault()); + } + + [HttpPut] + [HttpPost] + [Route("")] + [ProducesResponseType((int) HttpStatusCode.Created)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + public async Task AddAsync() + { + return Ok(memoryList.FirstOrDefault()); + } + + + [HttpDelete] + [Route("{id}")] + [ProducesResponseType((int) HttpStatusCode.NoContent)] + public async Task RemoveAsync(long id, CancellationToken ct) + { + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/IServiceUrl.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/IServiceUrl.cs new file mode 100644 index 00000000..77b88296 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/IServiceUrl.cs @@ -0,0 +1,8 @@ +namespace O2NextGen.CertificateManagement.Application; + +public interface IServiceUrl +{ + string IdentityUrl { get; set; } + + string CGenUrl { get; set; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/IoC/ServiceCollectionExtensions.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/IoC/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..4aefec4a --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/IoC/ServiceCollectionExtensions.cs @@ -0,0 +1,67 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Infrastructure.Data; + +namespace O2NextGen.CertificateManagement.Application.IoC +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddBusiness(this IServiceCollection services) + { + services.AddMediatR( + typeof(CertificateQuery)); + + return services; + } + + public static TConfig ConfigurePOCO(this IServiceCollection services, IConfiguration configuration) + where TConfig : class, new() + { + if (services == null) throw new ArgumentNullException(nameof(services)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + var config = new TConfig(); + configuration.Bind(config); + services.AddSingleton(config); + return config; + } + + public static IServiceCollection AddConfigEf(this IServiceCollection services, ConfigurationManager configuration) + { + var connectionString = configuration["ConnectionString"]; + services.AddDbContext(x => + x.UseSqlServer(connectionString)); + return services; + } + + public static IServiceCollection AddInfrastructure(this IServiceCollection services) + { + //services.AddSingleton(); + + services.Scan(scan => + scan + .FromAssembliesOf(typeof(CGenDbContext)) + .AddClasses(classes => classes.AssignableTo(typeof(IRepository<>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + .AddClasses(classes => classes.AssignableTo(typeof(IQueryHandler<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime() + ); + + // without Scrutor (or an alternative) we'd need to do everything by hand, like: + /* + services.AddScoped, EfRepository>(); + services.AddScoped, EfRepository>(); + services.AddScoped>, UserGroupsQueryHandler>(); + services.AddScoped, UserGroupQueryHandler>(); + // ... + */ + return services; + } + } + +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Logging/ElasticJsonFormatter.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Logging/ElasticJsonFormatter.cs new file mode 100644 index 00000000..57a6457c --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Logging/ElasticJsonFormatter.cs @@ -0,0 +1,6 @@ +namespace O2NextGen.CertificateManagement.Application.Logging; + +public class ElasticJsonFormatter +{ + +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj new file mode 100644 index 00000000..0aec1770 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.csproj @@ -0,0 +1,56 @@ + + + + net6.0 + disable + enable + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.v3.ncrunchproject new file mode 100644 index 00000000..44cdd50f --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/O2NextGen.CertificateManagement.Application.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + True + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Program.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Program.cs new file mode 100644 index 00000000..b14b74a0 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Program.cs @@ -0,0 +1,52 @@ +using O2NextGen.CertificateManagement.Application; +using O2NextGen.CertificateManagement.Application.IoC; +using O2NextGen.CertificateManagement.Infrastructure.Data; +using O2NextGen.CertificateManagement.StartupTasks.DatabaseInitializer; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; +var environment = builder.Environment; + +// Add services to the container. +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services + .AddConfigEf(configuration) + .AddDatabaseInitializer() + .AddBusiness() + .AddInfrastructure(); + +var app = builder.Build(); + +var scope = app.Services.CreateScope(); +var context = scope.ServiceProvider.GetRequiredService(); +SeedData.SeedAsync(context).Wait(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + + +} +app.UseSwagger(); +app.UseSwaggerUI(); +app.Use(async (context, next) => +{ + context.Response.OnStarting(() => + { + context.Response.Headers.Add("X-Powered-By", "O2NextGen Platform"); + return Task.CompletedTask; + }); + + await next.Invoke(); +}); +app.UseRouting(); +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Properties/launchSettings.json b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Properties/launchSettings.json new file mode 100644 index 00000000..c19fff4c --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "O2NextGen.CertificateManagement.Application": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:11001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/SeedData.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/SeedData.cs new file mode 100644 index 00000000..00686c2c --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/SeedData.cs @@ -0,0 +1,106 @@ +using Microsoft.EntityFrameworkCore; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Infrastructure.Data; +using O2NextGen.Sdk.NetCore.Extensions; + +namespace O2NextGen.CertificateManagement.Application; + +public class SeedData +{ + public static async Task SeedAsync(CGenDbContext context) + { + context.Database.Migrate(); + + if (!context.Categories.Any()) + { + context.Categories.AddRange( + GetPreconfiguredCategories()); + await context.SaveChangesAsync(); + } + + if (!context.Certificates.Any()) + { + context.Certificates.AddRange( + GetPreconfiguredCertificates(context)); + await context.SaveChangesAsync(); + } + } + + private static Certificate[] GetPreconfiguredCertificates(CGenDbContext cGenDbContext) + { + return new Certificate[] + { + new Certificate() + { + Category = cGenDbContext.Categories.FirstOrDefault(), + CustomerId = Guid.NewGuid().ToString(), + AddedDate = DateTime.Now.ConvertToUnixTime(), + IsVisible = true, + CategoryId = cGenDbContext.Categories.FirstOrDefault().Id, + CreatorId = Guid.NewGuid().ToString(), + ExpiredDate = DateTime.Now.AddYears(1).ConvertToUnixTime() + }, + new Certificate() + { + Category = cGenDbContext.Categories.FirstOrDefault(), + CustomerId = Guid.NewGuid().ToString(), + AddedDate = DateTime.Now.ConvertToUnixTime(), + IsVisible = true, + CategoryId = cGenDbContext.Categories.FirstOrDefault().Id, + CreatorId = Guid.NewGuid().ToString(), + ExpiredDate = DateTime.Now.AddYears(1).ConvertToUnixTime() + }, + new Certificate() + { + Category = cGenDbContext.Categories.FirstOrDefault(), + CustomerId = Guid.NewGuid().ToString(), + AddedDate = DateTime.Now.ConvertToUnixTime(), + IsVisible = true, + CategoryId = cGenDbContext.Categories.FirstOrDefault().Id, + CreatorId = Guid.NewGuid().ToString(), + ExpiredDate = DateTime.Now.AddYears(1).ConvertToUnixTime() + }, + new Certificate() + { + Category = cGenDbContext.Categories.FirstOrDefault(), + CustomerId = Guid.NewGuid().ToString(), + AddedDate = DateTime.Now.ConvertToUnixTime(), + IsVisible = true, + CategoryId = cGenDbContext.Categories.FirstOrDefault().Id, + CreatorId = Guid.NewGuid().ToString(), + ExpiredDate = DateTime.Now.AddYears(1).ConvertToUnixTime() + }, + + new Certificate() + { + Category = cGenDbContext.Categories.FirstOrDefault(), + CustomerId = Guid.NewGuid().ToString(), + AddedDate = DateTime.Now.ConvertToUnixTime(), + IsVisible = true, + CategoryId = cGenDbContext.Categories.FirstOrDefault().Id, + CreatorId = Guid.NewGuid().ToString(), + ExpiredDate = DateTime.Now.AddYears(1).ConvertToUnixTime() + } + }; + } + + private static Category[] GetPreconfiguredCategories() + { + return new Category[] { + new Category() + { + CategoryName = "AA Category", + CategorySeries = "A", + CategoryDescription = "desc category A", + QuantityCertificates = 120 + }, + new Category() + { + CategoryName = "B Category", + CategorySeries = "B", + CategoryDescription = "desc category B", + QuantityCertificates = 120 + } + }; + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/ServiceUrls.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/ServiceUrls.cs new file mode 100644 index 00000000..81148220 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/ServiceUrls.cs @@ -0,0 +1,8 @@ +namespace O2NextGen.CertificateManagement.Application; + +public class ServiceUrls: IServiceUrl +{ + public string IdentityUrl { get; set; } + + public string CGenUrl { get; set; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/CustomerService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/CustomerService.cs new file mode 100644 index 00000000..ca96efca --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/CustomerService.cs @@ -0,0 +1,19 @@ +namespace O2NextGen.CertificateManagement.Application.Services +{ + public class CustomerService : ICustomerService + { + // ReSharper disable once NotAccessedField.Local + private IHttpContextAccessor _context; + + public CustomerService(IHttpContextAccessor context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public Guid CustomerId { get; } = Guid.Parse("A6D86517-CF70-4EEF-8340-BCBAA4B60C4A"); + public string CustomerDescription { get; set; } = "#PF_R Community"; + public string RegisterLink { get; set; } = "https://pfr-centr.com"; + public string AccountLink { get; set; } = "https://pfr-centr.com"; + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/ICustomerService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/ICustomerService.cs new file mode 100644 index 00000000..4921d27c --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/ICustomerService.cs @@ -0,0 +1,11 @@ +namespace O2NextGen.CertificateManagement.Application.Services +{ + public interface ICustomerService + { + Guid CustomerId { get; } + string CustomerDescription { get; set; } + string RegisterLink { get; set; } + string AccountLink { get; set; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/IIdentityService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/IIdentityService.cs new file mode 100644 index 00000000..73f75c43 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/IIdentityService.cs @@ -0,0 +1,10 @@ +namespace O2NextGen.CertificateManagement.Application.Services +{ + public interface IIdentityService + { + string GetUserIdentity(); + + string GetUserName(); + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/IdentityService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/IdentityService.cs new file mode 100644 index 00000000..67a65962 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/Services/IdentityService.cs @@ -0,0 +1,23 @@ +namespace O2NextGen.CertificateManagement.Application.Services +{ + public class IdentityService : IIdentityService + { + private IHttpContextAccessor _context; + + public IdentityService(IHttpContextAccessor context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public string GetUserIdentity() + { + return _context.HttpContext.User.FindFirst("sub").Value; + } + + public string GetUserName() + { + return _context.HttpContext.User.Identity.Name; + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/appsettings.Development.json b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/appsettings.Development.json new file mode 100644 index 00000000..43bc444f --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/appsettings.Development.json @@ -0,0 +1,16 @@ +{ + "DatabaseInitializerSettings": { + "Initialize": true + }, + "ConnectionString": "Server=52.185.108.164;Initial Catalog=O2NextGen.CertificateDb-Dev;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30;", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + }, + "Urls": { + "Auth": "http://localhost:10001" + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Application/appsettings.json b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/appsettings.json new file mode 100644 index 00000000..bcc9a7da --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Application/appsettings.json @@ -0,0 +1,17 @@ +{ + "DatabaseInitializerSettings": { + "Initialize": true + }, + "ConnectionString": "Server=52.185.108.164;Initial Catalog=O2NextGen.CertificateDb;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30;", + "Urls": { + "Auth": "http://localhost:10001" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Models/Category.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Models/Category.cs new file mode 100644 index 00000000..73bd17d9 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Models/Category.cs @@ -0,0 +1,9 @@ +namespace O2NextGen.CertificateManagement.Business.Models +{ + public class Category + { + public long Id { get; set; } + public string Name { get; set; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.csproj index 37c6e8da..e012af3e 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.csproj +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2 + net6.0 diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/O2NextGen.CertificateManagement.Business.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Services/ICategoryService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Services/ICategoryService.cs new file mode 100644 index 00000000..1e98b36d --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Services/ICategoryService.cs @@ -0,0 +1,20 @@ +using O2NextGen.CertificateManagement.Business.Models; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace O2NextGen.CertificateManagement.Business.Services +{ + public interface ICategoryService + { + Task> GetAllAsync(CancellationToken ct); + + Task GetByIdAsync(long id, CancellationToken ct); + + Task UpdateAsync(Category category, CancellationToken ct); + + Task AddAsync(Category category, CancellationToken ct); + + Task RemoveAsync(long id, CancellationToken ct); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Services/ICertificatesService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Services/ICertificatesService.cs index 26827ecb..0fc1e0cd 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Services/ICertificatesService.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Business/Services/ICertificatesService.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using O2NextGen.CertificateManagement.Business.Models; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using O2NextGen.CertificateManagement.Business.Models; namespace O2NextGen.CertificateManagement.Business.Services { @@ -14,7 +14,7 @@ public interface ICertificatesService Task UpdateAsync(Certificate certificate, CancellationToken ct); Task AddAsync(Certificate certificate, CancellationToken ct); - + Task RemoveAsync(long id, CancellationToken ct); } } \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/CertificateManagementDbContext.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/CertificateManagementDbContext.cs index 4e7ddc81..8fb7bc4b 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/CertificateManagementDbContext.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/CertificateManagementDbContext.cs @@ -35,7 +35,7 @@ private void ConfigureCertificateEntity(EntityTypeBuilder bui builder.Property(ci => ci.Id) .HasColumnType("bigint") - .ForSqlServerUseSequenceHiLo("certificate_hilo") + .UseHiLo("certificate_hilo") .IsRequired(); } diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/Entities/CategoryEntity.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/Entities/CategoryEntity.cs new file mode 100644 index 00000000..c82914d0 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/Entities/CategoryEntity.cs @@ -0,0 +1,8 @@ +namespace O2NextGen.CertificateManagement.Data.Entities +{ + public class CategoryEntity + { + public long Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/Entities/CertificateEntity.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/Entities/CertificateEntity.cs index 5d48f262..be555747 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/Entities/CertificateEntity.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/Entities/CertificateEntity.cs @@ -1,8 +1,8 @@ -namespace O2NextGen.CertificateManagement.Data.Entities -{ - public class CertificateEntity - { - public long Id { get; set; } - public string Name { get; set; } - } +namespace O2NextGen.CertificateManagement.Data.Entities +{ + public class CertificateEntity + { + public long Id { get; set; } + public string Name { get; set; } + } } \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.csproj index 68520b18..d5302b71 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.csproj +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.csproj @@ -1,14 +1,15 @@ - netcoreapp2.2 + net6.0 - - - - + + + + + + - diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Data/O2NextGen.CertificateManagement.Data.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IEntity.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IEntity.cs new file mode 100644 index 00000000..b04a206a --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IEntity.cs @@ -0,0 +1,7 @@ +namespace O2NextGen.CertificateManagement.Domain.Data +{ + public interface IEntity + { + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IQuery.cs new file mode 100644 index 00000000..9b2f9689 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IQuery.cs @@ -0,0 +1,8 @@ +namespace O2NextGen.CertificateManagement.Domain.Data +{ + public interface IQuery + { + + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IQueryHandler.cs new file mode 100644 index 00000000..ce841088 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IQueryHandler.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace O2NextGen.CertificateManagement.Domain.Data +{ + public interface IQueryHandler where TQuery : IQuery + { + Task HandleAsync(TQuery query, CancellationToken ct); + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IRepository.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IRepository.cs new file mode 100644 index 00000000..ad19cffd --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/IRepository.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace O2NextGen.CertificateManagement.Domain.Data +{ + public interface IRepository + { + Task AddAsync(T entity, CancellationToken ct); + Task UpdateAsync(T entity, CancellationToken ct); + Task DeleteAsync(T entity, CancellationToken ct); + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CategoriesQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CategoriesQuery.cs new file mode 100644 index 00000000..c152c550 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CategoriesQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.Data.Queries +{ + public class CategoriesQuery : IQuery> + { + + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CategoryQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CategoryQuery.cs new file mode 100644 index 00000000..50c70be8 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CategoryQuery.cs @@ -0,0 +1,18 @@ +namespace O2NextGen.CertificateManagement.Domain.Data.Queries +{ + public class CategoryQuery : IQuery + { + public long Id { get; set; } + public string CategoryName { get; set; } + public int QuantityCertificates { get; set; } + public string CategoryDescription { get; set; } + public string CategorySeries { get; set; } + + public CategoryQuery(long id) + { + Id = id; + } + + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CertificateQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CertificateQuery.cs new file mode 100644 index 00000000..1ac47ad2 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CertificateQuery.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.Data.Queries +{ + public class CertificateQuery : IQuery + { + public CertificateQuery(long id) + { + Id = id; + } + + public CertificateQuery(string externalId, bool? isDeleted, string customerId, long expiredDate, long publishDate, + string creatorId, string publishCode, bool isVisible, long categoryId, Category category, + bool @lock, long lockedDate, string lockInfo, ICollection languageInfos) + { + ExternalId = externalId; + IsDeleted = isDeleted; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos = languageInfos; + } + + public long Id { get; } + public string ExternalId { get; } + public bool? IsDeleted { get; } + public string CustomerId { get; } + public long ExpiredDate { get; } + public long PublishDate { get; } + public string CreatorId { get; } + public string PublishCode { get; } + public bool IsVisible { get; } + public long CategoryId { get; } + public Category Category { get; } + public bool Lock { get; } + public long LockedDate { get; } + public string LockInfo { get; } + public ICollection LanguageInfos { get; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CertificatesQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CertificatesQuery.cs new file mode 100644 index 00000000..ccc4d174 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Data/Queries/CertificatesQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.Data.Queries +{ + public class CertificatesQuery : IQuery> + { + + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/Category.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/Category.cs new file mode 100644 index 00000000..9312d93f --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/Category.cs @@ -0,0 +1,19 @@ +namespace O2NextGen.CertificateManagement.Domain.Entities +{ + public class Category + { + public long Id { get; set; } + public long ModifiedDate { get; set; } + public long AddedDate { get; set; } + public long? DeletedDate { get; set; } + public bool? IsDeleted { get; set; } + + public string CustomerId { get; set; } + public string CategorySeries { get; set; } + public int QuantityCertificates { get; set; } + public int QuantityPublishCode { get; set; } + public string CategoryName { get; set; } + public string CategoryDescription { get; set; } + public int TimeLifeInDays { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/Certificate.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/Certificate.cs new file mode 100644 index 00000000..ed5e2920 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/Certificate.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace O2NextGen.CertificateManagement.Domain.Entities +{ + public class Certificate + { + public long Id { get; set; } + public string ExternalId { get; set; } + public long ModifiedDate { get; set; } + public long AddedDate { get; set; } + public long? DeletedDate { get; set; } + public bool? IsDeleted { get; set; } + + public string OwnerAccountId { get; set; } + public string CustomerId { get; set; } + public long ExpiredDate { get; set; } + public long PublishDate { get; set; } + public string CreatorId { get; set; } + public string PublishCode { get; set; } + public bool IsVisible { get; set; } + + public long CategoryId { get; set; } + public Category Category { get; set; } + public bool Lock { get; set; } + public long LockedDate { get; set; } + public string LockInfo { get; set; } + + public ICollection LanguageInfos { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/LanguageInfo.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/LanguageInfo.cs new file mode 100644 index 00000000..606549f3 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Entities/LanguageInfo.cs @@ -0,0 +1,18 @@ +namespace O2NextGen.CertificateManagement.Domain.Entities +{ + public class LanguageInfo + { + public long Id { get; set; } + public long ModifiedDate { get; set; } + public long AddedDate { get; set; } + public long? DeletedDate { get; set; } + public bool? IsDeleted { get; set; } + + public long CertificateId { get; set; } + public int LanguageId { get; set; } + public Certificate Certificate { get; set; } + public string Lastname { get; set; } + public string Firstname { get; set; } + public string Middlename { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Mappings/MappingExtensions.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Mappings/MappingExtensions.cs new file mode 100644 index 00000000..6bba06fd --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/Mappings/MappingExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace O2NextGen.CertificateManagement.Domain.Mappings +{ + internal static class MappingExtensions + { + public static IReadOnlyCollection MapAsReadOnly(this IEnumerable toMap, Func map) + { + _ = toMap ?? throw new ArgumentNullException(nameof(toMap)); + _ = map ?? throw new ArgumentNullException(nameof(map)); + + return toMap switch + { + IReadOnlyCollection readOnly => MapWithKnownCount(readOnly, readOnly.Count, map), + ICollection collection => MapWithKnownCount(collection, collection.Count, map), + _ => toMap.Select(map).ToList().AsReadOnly() + }; + } + + private static IReadOnlyCollection MapWithKnownCount( + IEnumerable collection, + int count, + Func map) + { + if (count == 0) + { + return Array.Empty(); + } + + var result = new TOut[count]; + var i = 0; + foreach (var entry in collection) + { + result[i] = map(entry); + ++i; + } + + return new ReadOnlyCollection(result); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.csproj new file mode 100644 index 00000000..ad7caf94 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.csproj @@ -0,0 +1,36 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.v3.ncrunchproject new file mode 100644 index 00000000..44cdd50f --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/O2NextGen.CertificateManagement.Domain.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + True + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommand.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommand.cs new file mode 100644 index 00000000..6f93316c --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommand.cs @@ -0,0 +1,30 @@ +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.CreateCategory +{ + public class CreateCategoryCommand : IRequest + { + private readonly long _id; + + public CreateCategoryCommand(string categoryName, int quantityCertificates, + string categoryDescription, string categorySeries) + { + CategoryName = categoryName; + QuantityCertificates = quantityCertificates; + CategoryDescription = categoryDescription; + CategorySeries = categorySeries; + } + + public string CategoryName { get; } + public string CategoryDescription { get; } + public string CategorySeries { get; } + public string CustomerId { get; internal set; } + public long AddedDate { get; internal set; } + public long ModifiedDate { get; internal set; } + public long? DeletedDate { get; internal set; } + public bool? IsDeleted { get; internal set; } + public int TimeLifeInDays { get; internal set; } + public int QuantityCertificates { get; } + public int QuantityPublishCode { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommandHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommandHandler.cs new file mode 100644 index 00000000..6f0eebae --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommandHandler.cs @@ -0,0 +1,53 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.CreateCategory +{ + public class CreateCategoryCommandHandler : IRequestHandler + { + private readonly IRepository categoryRepository; + + public CreateCategoryCommandHandler(IRepository categoryRepository) + { + this.categoryRepository = categoryRepository; + } + + public async Task Handle(CreateCategoryCommand request, CancellationToken cancellationToken) + { + var category = new Entities.Category + { + CategoryName = request.CategoryName, + CategoryDescription = request.CategoryDescription, + CategorySeries = request.CategorySeries, + CustomerId = request.CustomerId, + AddedDate = request.AddedDate, + ModifiedDate = request.ModifiedDate, + DeletedDate = request.DeletedDate, + IsDeleted = request.IsDeleted, + TimeLifeInDays = request.TimeLifeInDays, + QuantityCertificates = request.QuantityCertificates, + QuantityPublishCode = request.QuantityPublishCode + }; + + var addedCertificate = await categoryRepository.AddAsync(category, cancellationToken); + + return new CreateCategoryCommandResult( + addedCertificate.Id, + addedCertificate.CategoryName, + addedCertificate.CategoryDescription, + addedCertificate.CategorySeries, + addedCertificate.CustomerId, + addedCertificate.AddedDate, + addedCertificate.ModifiedDate, + addedCertificate.DeletedDate, + addedCertificate.IsDeleted, + addedCertificate.TimeLifeInDays, + addedCertificate.QuantityCertificates, + addedCertificate.QuantityPublishCode + ); + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommandResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommandResult.cs new file mode 100644 index 00000000..21213149 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/CreateCategory/CreateCategoryCommandResult.cs @@ -0,0 +1,34 @@ +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.CreateCategory +{ + public class CreateCategoryCommandResult + { + public CreateCategoryCommandResult(long id, string categoryName, string categoryDescription, string categorySeries, string customerId, long addedDate, long modifiedDate, long? deletedDate, bool? isDeleted, int timeLifeInDays, int quantityCertificates, int quantityPublishCode) + { + Id = id; + CategoryName = categoryName; + CategoryDescription = categoryDescription; + CategorySeries = categorySeries; + CustomerId = customerId; + AddedDate = addedDate; + ModifiedDate = modifiedDate; + DeletedDate = deletedDate; + IsDeleted = isDeleted; + TimeLifeInDays = timeLifeInDays; + QuantityCertificates = quantityCertificates; + QuantityPublishCode = quantityPublishCode; + } + + public long Id { get; } + public string CategoryName { get; } + public string CategoryDescription { get; } + public string CategorySeries { get; } + public string CustomerId { get; } + public long AddedDate { get; } + public long ModifiedDate { get; } + public long? DeletedDate { get; } + public bool? IsDeleted { get; } + public int TimeLifeInDays { get; } + public int QuantityCertificates { get; } + public int QuantityPublishCode { get; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/DeleteCategory/DeleteCategoryCommand.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/DeleteCategory/DeleteCategoryCommand.cs new file mode 100644 index 00000000..f4c472b8 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/DeleteCategory/DeleteCategoryCommand.cs @@ -0,0 +1,15 @@ + +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.DeleteCategory +{ + public class DeleteCategoryCommand : IRequest + { + public DeleteCategoryCommand(long id) + { + Id = id; + } + public long Id { get; set; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/DeleteCategory/DeleteCategoryCommandHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/DeleteCategory/DeleteCategoryCommandHandler.cs new file mode 100644 index 00000000..af52f362 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/DeleteCategory/DeleteCategoryCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.DeleteCategory; + +public class DeleteCategoryCommandHandler : IRequestHandler +{ + private readonly IQueryHandler _userGroupQueryHandler; + private readonly IRepository groupsRepository; + + public DeleteCategoryCommandHandler( + IQueryHandler userGroupQueryHandler, + IRepository groupsRepository) + { + _userGroupQueryHandler = + userGroupQueryHandler ?? throw new ArgumentNullException(nameof(userGroupQueryHandler)); + this.groupsRepository = groupsRepository; + } + + public async Task Handle(DeleteCategoryCommand request, CancellationToken cancellationToken) + { + var category = await _userGroupQueryHandler.HandleAsync( + new CategoryQuery(request.Id), + cancellationToken); + + if (category is object) + { + await groupsRepository.DeleteAsync(category, cancellationToken); + } + + return Unit.Value; + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQuery.cs new file mode 100644 index 00000000..5056ad66 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQuery.cs @@ -0,0 +1,7 @@ +using MediatR; +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategories +{ + public class GetCategoriesQuery : IRequest + { + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQueryHandler.cs new file mode 100644 index 00000000..85d74d35 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Domain.Mappings; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategories +{ + + public class GetCategoriesQueryHandler + : IRequestHandler + { + private readonly IQueryHandler> queryHandler; + + public GetCategoriesQueryHandler(IQueryHandler> queryHandler) + { + this.queryHandler = queryHandler; + } + + public async Task Handle(GetCategoriesQuery request, + CancellationToken cancellationToken) + { + var certificates = await queryHandler.HandleAsync( + new CategoriesQuery(), + cancellationToken); + + return new GetCategoriesQueryResult( + certificates.MapAsReadOnly(category => + new GetCategoriesQueryResult.CategoryViewModel( + category.Id, + category.CategoryName, + category.CategoryDescription, + category.CategorySeries, + category.QuantityCertificates))); + + + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQueryResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQueryResult.cs new file mode 100644 index 00000000..16bc8a49 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategories/GetCategoriesQueryResult.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategories +{ + + public class GetCategoriesQueryResult + { + public GetCategoriesQueryResult(IReadOnlyCollection categories) + { + Categories = categories; + } + + public IReadOnlyCollection Categories { get; } + + public class CategoryViewModel + { + public long Id { get; set; } + public string CategoryName { get; } + public string CategoryDescription { get; } + public string CategorySeries { get; } + public int QuantityCertificates { get; } + + public CategoryViewModel(long id, string categoryName, string categoryDescription, + string categorySeries, int quantityCertificates) + { + Id = id; + CategoryName = categoryName; + CategoryDescription = categoryDescription; + CategorySeries = categorySeries; + QuantityCertificates = quantityCertificates; + } + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQuery.cs new file mode 100644 index 00000000..dd922b80 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQuery.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategory +{ + + public sealed class GetCategoryQuery : IRequest + { + public GetCategoryQuery(long id) + { + Id = id; + } + + public long Id { get; } + public string CategoryName { get; set; } + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQueryHandler.cs new file mode 100644 index 00000000..150808bf --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQueryHandler.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategory; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategory +{ + + public class GetCategoryQueryHandler : + IRequestHandler + { + private readonly IQueryHandler _queryHandler; + + public GetCategoryQueryHandler(IQueryHandler _queryHandler) + { + this._queryHandler = _queryHandler; + } + + public async Task Handle(GetCategoryQuery request, CancellationToken cancellationToken) + { + var category = await _queryHandler.HandleAsync( + new CategoryQuery( + request.Id), + cancellationToken); + + if (category is null) + { + return null; + } + + return new GetCategoryQueryResult( + id: category.Id, + modifiedDate: category.ModifiedDate, + addedDate: category.AddedDate, + deletedDate: category.DeletedDate, + isDeleted: category.IsDeleted, + customerId: category.CustomerId, + categoryName: category.CategoryName, + categoryDescription: category.CategoryDescription, + quantityCertificates: category.QuantityCertificates, + quantityPublishCode: category.QuantityPublishCode, + categorySeries: category.CategorySeries); + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQueryResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQueryResult.cs new file mode 100644 index 00000000..3d3535fd --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/GetCategory/GetCategoryQueryResult.cs @@ -0,0 +1,39 @@ +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategory +{ + public class GetCategoryQueryResult + { + public GetCategoryQueryResult( + long id, long modifiedDate, long addedDate, long? deletedDate, bool? isDeleted, + string customerId, + string categoryName, + string categoryDescription, + int quantityCertificates, + int quantityPublishCode, string categorySeries) + { + Id = id; + ModifiedDate = modifiedDate; + AddedDate = addedDate; + DeletedDate = deletedDate; + IsDeleted = isDeleted; + CustomerId = customerId; + CategoryName = categoryName; + CategoryDescription = categoryDescription; + QuantityCertificates = quantityCertificates; + QuantityPublishCode = quantityPublishCode; + CategorySeries = categorySeries; + } + + + public long Id { get; } + public long ModifiedDate { get; } + public long AddedDate { get; } + public long? DeletedDate { get; } + public bool? IsDeleted { get; } + public string CustomerId { get; } + public string CategoryName { get; } + public string CategoryDescription { get; } + public int QuantityCertificates { get; } + public int QuantityPublishCode { get; } + public string CategorySeries { get; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommand.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommand.cs new file mode 100644 index 00000000..f23e62f0 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommand.cs @@ -0,0 +1,37 @@ +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.UpdateCategory; + +public class UpdateCategoryDetailsCommand : IRequest +{ + public UpdateCategoryDetailsCommand( + long id, + string categoryName, + string categoryDescription, + string categorySeries, + string customerId, + int quantityCertificates, + int quantityPublishCode) + { + Id = id; + CategoryName = categoryName; + CategoryDescription = categoryDescription; + CategorySeries = categorySeries; + CustomerId = customerId; + QuantityCertificates = quantityCertificates; + QuantityPublishCode = quantityPublishCode; + } + + public string CategoryName { get; set; } + public string CategoryDescription { get; set; } + public string CategorySeries { get; set; } + public string CustomerId { get; set; } + public long? AddedDate { get; set; } + public long? ModifiedDate { get; set; } + public long? DeletedDate { get; set; } + public bool? IsDeleted { get; set; } + public int TimeLifeInDays { get; set; } + public int QuantityCertificates { get; set; } + public int QuantityPublishCode { get; set; } + public long Id { get; } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommandHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommandHandler.cs new file mode 100644 index 00000000..4e7f9ac3 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommandHandler.cs @@ -0,0 +1,63 @@ +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.UpdateCategory; + +public class UpdateCategoryDetailsCommandHandler + : IRequestHandler +{ + private readonly IQueryHandler _userGroupQueryHandler; + private readonly IRepository _groupsRepository; + + public UpdateCategoryDetailsCommandHandler( + IQueryHandler userGroupQueryHandler, + IRepository groupsRepository) + { + _userGroupQueryHandler = + userGroupQueryHandler ?? throw new ArgumentNullException(nameof(userGroupQueryHandler)); + _groupsRepository = groupsRepository ?? throw new ArgumentNullException(nameof(groupsRepository)); + } + + public async Task Handle(UpdateCategoryDetailsCommand request, + CancellationToken cancellationToken) + { + var category = await _userGroupQueryHandler.HandleAsync( + new CategoryQuery( + request.Id + ), + cancellationToken); + + if (category is null) + { + return null; + } + + + category.IsDeleted = request.IsDeleted; + category.CustomerId = request.CustomerId; + category.CategoryName = request.CategoryName; + + category.CategoryName = request.CategoryName; + + + category.CategoryDescription = request.CategoryDescription; + category.QuantityCertificates = request.QuantityCertificates; + category.CategorySeries = request.CategorySeries; + + await _groupsRepository.UpdateAsync(category, cancellationToken); + + return new UpdateCategoryDetailsCommandResult( + category.Id, + category.ModifiedDate, + category.AddedDate, + category.DeletedDate, + category.IsDeleted, + category.CustomerId, + category.CategoryName, + category.CategoryDescription, + category.QuantityCertificates, + category.QuantityPublishCode + ); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommandResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommandResult.cs new file mode 100644 index 00000000..4d845c65 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCategory/UpdateCategory/UpdateCategoryDetailsCommandResult.cs @@ -0,0 +1,35 @@ +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.UpdateCategory; + +public class UpdateCategoryDetailsCommandResult +{ + public long Id { get; } + public long ModifiedDate { get; } + public long AddedDate { get; } + public long? DeletedDate { get; } + public bool? IsDeleted { get; } + public string CustomerId { get; } + public string CategoryName { get; } + public string CategoryDescription { get; } + public int QuantityCertificates { get; } + public int QuantityPublishCode { get; } + + public UpdateCategoryDetailsCommandResult(long id, + long modifiedDate, + long addedDate, + long? deletedDate, + bool? isDeleted, string customerId, string categoryName, + string categoryDescription, + int quantityCertificates, int quantityPublishCode) + { + Id = id; + ModifiedDate = modifiedDate; + AddedDate = addedDate; + DeletedDate = deletedDate; + IsDeleted = isDeleted; + CustomerId = customerId; + CategoryName = categoryName; + CategoryDescription = categoryDescription; + QuantityCertificates = quantityCertificates; + QuantityPublishCode = quantityPublishCode; + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommand.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommand.cs new file mode 100644 index 00000000..0c60e067 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommand.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.CreateCertificate +{ + public class CreateCertificateCommand : IRequest + { + public string ExternalId { get; set; } + public bool? IsDeleted { get; set; } + + public string OwnerAccountId { get; set; } + public string CustomerId { get; set; } + public long ExpiredDate { get; set; } + public long PublishDate { get; set; } + public string CreatorId { get; set; } + public string PublishCode { get; set; } + public bool IsVisible { get; set; } + + public long CategoryId { get; set; } + public Category Category { get; set; } + public bool Lock { get; set; } + public long LockedDate { get; set; } + public string LockInfo { get; set; } + public ICollection LanguageInfos { get; } + + public CreateCertificateCommand( + string externalId, bool? IsDeleted, string ownerAccountId, string customerId, + long expiredDate, long publishDate, string creatorId, string publishCode, bool isVisible, + long categoryId, Category category, bool @lock, long lockedDate, string lockInfo, + ICollection languageInfos) + { + ExternalId = externalId; + this.IsDeleted = IsDeleted; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos = languageInfos; + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommandHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommandHandler.cs new file mode 100644 index 00000000..6dca818d --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommandHandler.cs @@ -0,0 +1,59 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.CreateCertificate +{ + public class CreateCertificateCommandHandler + : IRequestHandler + { + private readonly IRepository certificatesRepository; + + public CreateCertificateCommandHandler(IRepository certificatesRepository) + { + this.certificatesRepository = certificatesRepository; + } + public async Task Handle( + CreateCertificateCommand request, CancellationToken cancellationToken) + { + var certificate = new Entities.Certificate + { + ExternalId = request.ExternalId, + IsDeleted = request.IsDeleted, + CustomerId = request.CustomerId, + ExpiredDate = request.ExpiredDate, + PublishDate = request.PublishDate, + CreatorId = request.CreatorId, + PublishCode = request.PublishCode, + IsVisible = request.IsVisible, + CategoryId = request.CategoryId, + Category = request.Category, + Lock = request.Lock, + LockedDate = request.LockedDate, + LockInfo = request.LockInfo, + LanguageInfos = request.LanguageInfos + }; + + var addedCertificate = await certificatesRepository.AddAsync(certificate, cancellationToken); + + return new CreateCertificateCommandResult( + addedCertificate.ExternalId, + addedCertificate.IsDeleted, + addedCertificate.OwnerAccountId, + addedCertificate.CustomerId, + addedCertificate.ExpiredDate, + addedCertificate.PublishDate, + addedCertificate.CreatorId, + addedCertificate.PublishCode, + addedCertificate.IsVisible, + addedCertificate.CategoryId, + addedCertificate.Category, + addedCertificate.Lock, + addedCertificate.LockedDate, + addedCertificate.LockInfo, + addedCertificate.LanguageInfos); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommandResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommandResult.cs new file mode 100644 index 00000000..362d75b6 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/CreateCertificate/CreateCertificateCommandResult.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.CreateCertificate +{ + public class CreateCertificateCommandResult + { + public CreateCertificateCommandResult( + string externalId, bool? isDeleted, string ownerAccountId, string customerId, + long expiredDate, long publishDate, string creatorId, string publishCode, + bool isVisible, long categoryId, Category category, bool @lock, + long lockedDate, string lockInfo, ICollection languageInfos) + { + ExternalId = externalId; + IsDeleted = isDeleted; + OwnerAccountId = ownerAccountId; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos = languageInfos; + } + + public long Id { get; set; } + public string ExternalId { get; } + public long ModifiedDate { get; set; } + public long AddedDate { get; set; } + public long? DeletedDate { get; set; } + public bool? IsDeleted { get; } + public string OwnerAccountId { get; } + public string CustomerId { get; } + public long ExpiredDate { get; } + public long PublishDate { get; } + public string CreatorId { get; } + public string PublishCode { get; } + public bool IsVisible { get; } + public long CategoryId { get; } + public Category Category { get; } + public bool Lock { get; } + public long LockedDate { get; } + public string LockInfo { get; } + public ICollection LanguageInfos { get; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/DeleteCertificate/DeleteCertificateCommand.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/DeleteCertificate/DeleteCertificateCommand.cs new file mode 100644 index 00000000..82950f37 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/DeleteCertificate/DeleteCertificateCommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.DeleteCertificate +{ + + public sealed class DeleteCertificateCommand : IRequest + { + public DeleteCertificateCommand(long id) + { + Id = id; + } + + public long Id { get; set; } + } + +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/DeleteCertificate/DeleteCertificateCommandHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/DeleteCertificate/DeleteCertificateCommandHandler.cs new file mode 100644 index 00000000..ccf7172e --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/DeleteCertificate/DeleteCertificateCommandHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.DeleteCertificate +{ + + public class DeleteCertificateCommandHandler : IRequestHandler + { + private readonly IQueryHandler _userGroupQueryHandler; + private readonly IRepository groupsRepository; + + public DeleteCertificateCommandHandler( + IQueryHandler userGroupQueryHandler, + IRepository groupsRepository) + { + _userGroupQueryHandler = + userGroupQueryHandler ?? throw new ArgumentNullException(nameof(userGroupQueryHandler)); + this.groupsRepository = groupsRepository; + } + + public async Task Handle(DeleteCertificateCommand request, CancellationToken cancellationToken) + { + var certificate = await _userGroupQueryHandler.HandleAsync( + new CertificateQuery(request.Id), + cancellationToken); + + if (certificate is object) + { + await groupsRepository.DeleteAsync(certificate, cancellationToken); + } + + return Unit.Value; + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQuery.cs new file mode 100644 index 00000000..c4331acc --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQuery.cs @@ -0,0 +1,18 @@ +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificate +{ + + public sealed class GetCertificateQuery : IRequest + { + private long id; + + public GetCertificateQuery(long id) + { + Id = id; + } + + public long Id { get => id; set => id = value; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQueryHandler.cs new file mode 100644 index 00000000..b30ed881 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQueryHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificate +{ + + public class GetCertificateQueryHandler : + IRequestHandler + { + private readonly IQueryHandler _queryHandler; + + public GetCertificateQueryHandler(IQueryHandler queryHandler) + { + _queryHandler = queryHandler ?? throw new ArgumentNullException(nameof(queryHandler)); + } + + public async Task Handle(GetCertificateQuery request, + CancellationToken cancellationToken) + { + var certificate = await _queryHandler.HandleAsync( + new CertificateQuery(request.Id), + cancellationToken); + + if (certificate is null) + { + return null; + } + + return new GetCertificateQueryResult( + certificate.Id, + certificate.ExternalId, + certificate.ModifiedDate, + certificate.AddedDate, + certificate.DeletedDate, + certificate.IsDeleted, + certificate.OwnerAccountId, + certificate.CustomerId, + certificate.ExpiredDate, + certificate.PublishDate, + certificate.CreatorId, + certificate.PublishCode, + certificate.IsVisible, + certificate.CategoryId, + certificate.Category, + certificate.Lock, + certificate.LockedDate, + certificate.LockInfo, + certificate.LanguageInfos); + } + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQueryResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQueryResult.cs new file mode 100644 index 00000000..d029a369 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificate/GetCertificateQueryResult.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificate +{ + public sealed class GetCertificateQueryResult + { + public GetCertificateQueryResult(long id, string externalId, long modifiedDate, long addedDate, + long? deletedDate, bool? isDeleted, string ownerAccountId, string customerId, long expiredDate, + long publishDate, string creatorId, string publishCode, bool isVisible, long categoryId, + Category category, bool @lock, long lockedDate, string lockInfo, + ICollection languageInfos) + { + Id = id; + ExternalId = externalId; + ModifiedDate = modifiedDate; + AddedDate = addedDate; + DeletedDate = deletedDate; + IsDeleted = isDeleted; + OwnerAccountId = ownerAccountId; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos = languageInfos; + } + + public long Id { get; } + public string ExternalId { get; } + public long ModifiedDate { get; } + public long AddedDate { get; } + public long? DeletedDate { get; } + public bool? IsDeleted { get; } + public string OwnerAccountId { get; } + public string CustomerId { get; } + public long ExpiredDate { get; } + public long PublishDate { get; } + public string CreatorId { get; } + public string PublishCode { get; } + public bool IsVisible { get; } + public long CategoryId { get; } + public Category Category { get; } + public bool Lock { get; } + public long LockedDate { get; } + public string LockInfo { get; } + public ICollection LanguageInfos { get; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQuery.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQuery.cs new file mode 100644 index 00000000..267a8999 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQuery.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificates +{ + public class GetCertificatesQuery : IRequest + { + + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQueryHandler.cs new file mode 100644 index 00000000..bc19e735 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQueryHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Domain.Mappings; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificates +{ + public class GetCertificatesQueryHandler + : IRequestHandler + { + private readonly IQueryHandler> queryHandler; + + public GetCertificatesQueryHandler( + IQueryHandler> queryHandler) + { + this.queryHandler = queryHandler ?? throw new ArgumentNullException(nameof(queryHandler)); + } + + public async Task Handle(GetCertificatesQuery request, CancellationToken cancellationToken) + { + var certificates = await queryHandler.HandleAsync( + new CertificatesQuery(), + cancellationToken); + + return new GetCertificatesQueryResult( + certificates.MapAsReadOnly(certificate => + new GetCertificatesQueryResult.CertificateViewModel( + certificate.Id, + certificate.ExternalId, + certificate.ModifiedDate, + certificate.AddedDate, + certificate.DeletedDate, + certificate.IsDeleted, + certificate.OwnerAccountId, + certificate.CustomerId, + certificate.ExpiredDate, + certificate.PublishDate, + certificate.CreatorId, + certificate.PublishCode, + certificate.IsVisible, + certificate.CategoryId, + certificate.Category, + certificate.Lock, + certificate.LockedDate, + certificate.LockInfo, + certificate.LanguageInfos))); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQueryResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQueryResult.cs new file mode 100644 index 00000000..b284af84 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/GetCertificates/GetCertificatesQueryResult.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.GetCertificates +{ + + public class GetCertificatesQueryResult + { + public GetCertificatesQueryResult(IReadOnlyCollection certificates) + { + Certificates = certificates; + } + + public IReadOnlyCollection Certificates { get; } + + + public class CertificateViewModel + { + + public CertificateViewModel( + long id, string externalId, + long modifiedDate, long addedDate, long? deletedDate, + bool? isDeleted, string ownerAccountId, string customerId, + long expiredDate, long publishDate, string creatorId, string publishCode, + bool isVisible, long categoryId, Category category, bool @lock, + long lockedDate, string lockInfo, ICollection languageInfos) + { + Id = id; + + ModifiedDate = modifiedDate; + AddedDate = addedDate; + DeletedDate = deletedDate; + IsDeleted = isDeleted; + OwnerAccountId = ownerAccountId; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos = languageInfos; + } + + public long Id { get; set; } + public long ModifiedDate { get; } + public long AddedDate { get; } + public long? DeletedDate { get; } + public bool? IsDeleted { get; } + public string OwnerAccountId { get; } + public string CustomerId { get; } + public long ExpiredDate { get; } + public long PublishDate { get; } + public string CreatorId { get; } + public string PublishCode { get; } + public bool IsVisible { get; } + public long CategoryId { get; } + public Category Category { get; } + public bool Lock { get; } + public long LockedDate { get; } + public string LockInfo { get; } + public ICollection LanguageInfos { get; } + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommand.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommand.cs new file mode 100644 index 00000000..b7c829d2 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommand.cs @@ -0,0 +1,21 @@ +using MediatR; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.NewCertificate +{ + public class NewCertificateCommand : IRequest + { + public NewCertificateCommand(string userId, long categoryId, int languageId, string customerId) + { + UserId = userId; + CategoryId = categoryId; + LanguageId = languageId; + CustomerId = customerId; + } + + public string UserId { get; } + public long CategoryId { get; } + public int LanguageId { get; } + public string CustomerId { get; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommandHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommandHandler.cs new file mode 100644 index 00000000..657e8e50 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommandHandler.cs @@ -0,0 +1,102 @@ +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.Sdk.NetCore.Extensions; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.NewCertificate +{ + public class NewCertificateCommandHandler : + IRequestHandler + { + private readonly IQueryHandler _queryCategoryHandler; + + public NewCertificateCommandHandler(IQueryHandler queryCategoryHandler) + { + this._queryCategoryHandler = queryCategoryHandler; + } + + public async Task Handle(NewCertificateCommand request, CancellationToken cancellationToken) + { + var category = await _queryCategoryHandler.HandleAsync(new CategoryQuery(request.CategoryId), cancellationToken); + + if (category == null) + throw new ArgumentException($"CategoryModel {nameof(category)} not found"); + + LanguageInfo language; + + switch (request.LanguageId) + { + case 1: + { + language = new LanguageInfo + { + LanguageId = request.LanguageId, + Firstname = string.Empty, + Lastname = string.Empty, + Middlename = string.Empty + }; + break; + } + case 2: + { + language = new LanguageInfo + { + LanguageId = request.LanguageId, + Firstname = string.Empty, + Lastname = string.Empty, + Middlename = string.Empty + }; + break; + } + case 3: + { + language = new LanguageInfo + { + LanguageId = request.LanguageId, + Firstname = string.Empty, + Lastname = string.Empty, + Middlename = string.Empty + }; + break; + } + case 4: + { + language = new LanguageInfo + { + LanguageId = request.LanguageId, + Firstname = string.Empty, + Lastname = string.Empty, + Middlename = string.Empty + }; + break; + } + + default: + { + throw new ArgumentException("languageId is not found"); + } + } + + var languageInfos = new List { language }; + + return new NewCertificateCommandResult( + AddedDate: DateTime.Now.ConvertToUnixTime(), + ModifiedDate: DateTime.Now.ConvertToUnixTime(), + PublishCode: "", + LockInfo: string.Empty, + LockedDate: 0, + ExpiredDate: DateTime.Now.AddDays(category.TimeLifeInDays).ConvertToUnixTime(), + PublishDate: DateTime.Now.ConvertToUnixTime(), + CreatorId: request.UserId, + CustomerId: request.CustomerId, + OwnerAccountId: Guid.Empty.ToString(), + DeletedDate: default(long), + CategoryId: request.CategoryId, + Category: category, + LanguageInfos: languageInfos + ); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommandResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommandResult.cs new file mode 100644 index 00000000..7ec07ce2 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/NewCertificate/NewCertificateCommandResult.cs @@ -0,0 +1,40 @@ +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.NewCertificate +{ + public class NewCertificateCommandResult + { + public NewCertificateCommandResult(long AddedDate, long ModifiedDate, string PublishCode, string LockInfo, int LockedDate, long ExpiredDate, long PublishDate, string CreatorId, string CustomerId, string OwnerAccountId, long DeletedDate, long CategoryId, Category Category, List LanguageInfos) + { + this.AddedDate = AddedDate; + this.ModifiedDate = ModifiedDate; + this.PublishCode = PublishCode; + this.LockInfo = LockInfo; + this.LockedDate = LockedDate; + this.ExpiredDate = ExpiredDate; + this.PublishDate = PublishDate; + this.CreatorId = CreatorId; + this.CustomerId = CustomerId; + this.OwnerAccountId = OwnerAccountId; + this.DeletedDate = DeletedDate; + this.CategoryId = CategoryId; + this.Category = Category; + this.LanguageInfos = LanguageInfos; + } + + public long AddedDate { get; } + public long ModifiedDate { get; } + public string PublishCode { get; } + public string LockInfo { get; } + public int LockedDate { get; } + public long ExpiredDate { get; } + public long PublishDate { get; } + public string CreatorId { get; } + public string CustomerId { get; } + public string OwnerAccountId { get; } + public long DeletedDate { get; } + public long CategoryId { get; } + public Category Category { get; } + public List LanguageInfos { get; } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommand.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommand.cs new file mode 100644 index 00000000..c1a0f016 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommand.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.UpdateCertificate +{ + public class UpdateCertificateDetailsCommand : IRequest + { + public UpdateCertificateDetailsCommand( + long id, + string externalId, bool? isDeleted, string ownerAccountId, string customerId, + long expiredDate, long publishDate, string creatorId, string publishCode, bool isVisible, + long categoryId, Category category, bool @lock, long lockedDate, string lockInfo, + ICollection languageInfos) + { + Id = id; + ExternalId = externalId; + IsDeleted = isDeleted; + OwnerAccountId = ownerAccountId; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos = languageInfos; + } + + public UpdateCertificateDetailsCommand(long id, string externalId, bool? modifiedDate, string addedDate, string deletedDate, bool? isDeleted, string ownerAccountId, string customerId, long expiredDate, long publishDate, string creatorId, string publishCode, bool isVisible, long categoryId, Category category, bool @lock, long lockedDate, string lockInfo, ICollection languageInfos) + { + Id = id; + ExternalId = externalId; + ModifiedDate = modifiedDate; + AddedDate = addedDate; + DeletedDate = deletedDate; + IsDeleted = isDeleted; + OwnerAccountId = ownerAccountId; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category1 = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos1 = languageInfos; + } + + public long Id { get; } + public string ExternalId { get; } + public bool? IsDeleted { get; } + public string OwnerAccountId { get; } + public string CustomerId { get; } + public long ExpiredDate { get; } + public long PublishDate { get; } + public string CreatorId { get; } + public string PublishCode { get; } + public bool IsVisible { get; } + public long CategoryId { get; } + public Category Category { get; } + public bool Lock { get; } + public long LockedDate { get; } + public string LockInfo { get; } + public ICollection LanguageInfos { get; } + public bool? ModifiedDate { get; } + public string AddedDate { get; } + public string DeletedDate { get; } + public Category Category1 { get; } + public ICollection LanguageInfos1 { get; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommandHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommandHandler.cs new file mode 100644 index 00000000..7f3ae141 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommandHandler.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; + +namespace O2NextGen.CertificateManagement.Domain.UseCases.ForCertificate.UpdateCertificate +{ + public class UpdateCertificateDetailsCommandHandler + : IRequestHandler + { + private readonly IQueryHandler _userGroupQueryHandler; + private readonly IRepository _groupsRepository; + + public UpdateCertificateDetailsCommandHandler( + IQueryHandler userGroupQueryHandler, + IRepository groupsRepository) + { + _userGroupQueryHandler = + userGroupQueryHandler ?? throw new ArgumentNullException(nameof(userGroupQueryHandler)); + _groupsRepository = groupsRepository ?? throw new ArgumentNullException(nameof(groupsRepository)); + } + + public async Task Handle(UpdateCertificateDetailsCommand request, CancellationToken cancellationToken) + { + var certificate = await _userGroupQueryHandler.HandleAsync( + new CertificateQuery( + request.ExternalId, + request.IsDeleted, + request.CustomerId, + request.ExpiredDate, + request.PublishDate, + request.CreatorId, + request.PublishCode, + request.IsVisible, + request.CategoryId, + request.Category, + request.Lock, + request.LockedDate, + request.LockInfo, + request.LanguageInfos), + cancellationToken); + + if (certificate is null) + { + return null; + } + + certificate.ExternalId = request.ExternalId; + certificate.IsDeleted = request.IsDeleted; + certificate.CustomerId = request.CustomerId; + certificate.ExpiredDate = request.ExpiredDate; + certificate.PublishDate = request.PublishDate; + certificate.CreatorId = request.CreatorId; + certificate.PublishCode = request.PublishCode; + certificate.IsVisible = request.IsVisible; + certificate.CategoryId = request.CategoryId; + certificate.Category = request.Category; + certificate.Lock = request.Lock; + certificate.LockedDate = request.LockedDate; + certificate.LockInfo = request.LockInfo; + certificate.LanguageInfos = request.LanguageInfos; + + + await _groupsRepository.UpdateAsync(certificate, cancellationToken); + + return new UpdateCertificateDetailsCommandResult( + certificate.Id, + certificate.ExternalId, + certificate.ModifiedDate, + certificate.AddedDate, + certificate.DeletedDate, + certificate.IsDeleted, + certificate.OwnerAccountId, + certificate.CustomerId, + certificate.ExpiredDate, + certificate.PublishDate, + certificate.CreatorId, + certificate.PublishCode, + certificate.IsVisible, + certificate.CategoryId, + certificate.Category, + certificate.Lock, + certificate.LockedDate, + certificate.LockInfo, + certificate.LanguageInfos); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommandResult.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommandResult.cs new file mode 100644 index 00000000..b3672e75 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Domain/UseCases/ForCertificate/UpdateCertificate/UpdateCertificateDetailsCommandResult.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using O2NextGen.CertificateManagement.Domain.Entities; + +public class UpdateCertificateDetailsCommandResult +{ + public UpdateCertificateDetailsCommandResult(long id, string name) + { + Id = id; + Name = name; + } + + public UpdateCertificateDetailsCommandResult(long id, string name, long modifiedDate, long addedDate, long? deletedDate, bool? isDeleted, string ownerAccountId, string customerId, long expiredDate, long publishDate, string creatorId, string publishCode, bool isVisible, long categoryId, Category category, bool @lock, long lockedDate, string lockInfo, ICollection languageInfos) : this(id, name) + { + ModifiedDate = modifiedDate; + AddedDate = addedDate; + DeletedDate = deletedDate; + IsDeleted = isDeleted; + OwnerAccountId = ownerAccountId; + CustomerId = customerId; + ExpiredDate = expiredDate; + PublishDate = publishDate; + CreatorId = creatorId; + PublishCode = publishCode; + IsVisible = isVisible; + CategoryId = categoryId; + Category = category; + Lock = @lock; + LockedDate = lockedDate; + LockInfo = lockInfo; + LanguageInfos = languageInfos; + } + + public long Id { get; } + public string Name { get; } + public long ModifiedDate { get; } + public long AddedDate { get; } + public long? DeletedDate { get; } + public bool? IsDeleted { get; } + public string OwnerAccountId { get; } + public string CustomerId { get; } + public long ExpiredDate { get; } + public long PublishDate { get; } + public string CreatorId { get; } + public string PublishCode { get; } + public bool IsVisible { get; } + public long CategoryId { get; } + public Category Category { get; } + public bool Lock { get; } + public long LockedDate { get; } + public string LockInfo { get; } + public ICollection LanguageInfos { get; } +} + + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Mappings/CategoryMappings.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Mappings/CategoryMappings.cs new file mode 100644 index 00000000..74b08176 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Mappings/CategoryMappings.cs @@ -0,0 +1,23 @@ +using O2NextGen.CertificateManagement.Business.Models; +using O2NextGen.CertificateManagement.Data.Entities; +using System.Collections.Generic; + +namespace O2NextGen.CertificateManagement.Impl.Mappings +{ + internal static class CategoryMappings + { + public static Category ToService(this CategoryEntity entity) + { + return entity != null ? new Category() { Id = entity.Id, Name = entity.Name } : null; + } + + public static CategoryEntity ToEntity(this Category model) + { + return model != null ? new CategoryEntity() { Id = model.Id, Name = model.Name } : null; + } + + public static IReadOnlyCollection + ToService(this IReadOnlyCollection entities) => + entities.MapCollection(ToService); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Mappings/CertificateMappings.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Mappings/CertificateMappings.cs index 4d32e5a1..9fed54b7 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Mappings/CertificateMappings.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Mappings/CertificateMappings.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using O2NextGen.CertificateManagement.Business.Models; using O2NextGen.CertificateManagement.Data.Entities; +using System.Collections.Generic; namespace O2NextGen.CertificateManagement.Impl.Mappings { @@ -8,12 +8,12 @@ internal static class CertificateMappings { public static Certificate ToService(this CertificateEntity entity) { - return entity != null ? new Certificate() {Id = entity.Id, Name = entity.Name} : null; + return entity != null ? new Certificate() { Id = entity.Id, Name = entity.Name } : null; } public static CertificateEntity ToEntity(this Certificate model) { - return model != null ? new CertificateEntity() {Id = model.Id, Name = model.Name} : null; + return model != null ? new CertificateEntity() { Id = model.Id, Name = model.Name } : null; } public static IReadOnlyCollection diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.csproj index c29078c1..23207484 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.csproj +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2 + net6.0 diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/O2NextGen.CertificateManagement.Impl.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/CertificatesService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/CertificatesService.cs index 7b8ff99a..0c21552b 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/CertificatesService.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/CertificatesService.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using O2NextGen.CertificateManagement.Business.Models; using O2NextGen.CertificateManagement.Business.Services; using O2NextGen.CertificateManagement.Data; using O2NextGen.CertificateManagement.Impl.Mappings; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace O2NextGen.CertificateManagement.Impl.Services { @@ -30,7 +30,7 @@ public CertificatesService(CertificateManagementDbContext context) public async Task> GetAllAsync(CancellationToken ct) { - var certificates = await _context.Certificates.AsNoTracking().OrderBy(_=>_.Id).ToListAsync(); + var certificates = await _context.Certificates.AsNoTracking().OrderBy(_ => _.Id).ToListAsync(); return certificates.ToService(); } @@ -45,8 +45,8 @@ public async Task UpdateAsync(Certificate certificate, Cancellation var updatedCertificateEntity = _context.Certificates.Update(certificate.ToEntity()); await _context.SaveChangesAsync(ct); return updatedCertificateEntity.Entity.ToService(); - } - + } + public async Task AddAsync(Certificate certificate, CancellationToken ct) { var addedCertificateEntity = await _context.Certificates.AddAsync(certificate.ToEntity(), ct); diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/InMemoryCategoriesService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/InMemoryCategoriesService.cs new file mode 100644 index 00000000..386ab833 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/InMemoryCategoriesService.cs @@ -0,0 +1,73 @@ +using O2NextGen.CertificateManagement.Business.Models; +using O2NextGen.CertificateManagement.Business.Services; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace O2NextGen.CertificateManagement.Impl.Services +{ + public class InMemoryCategoriesService : ICategoryService + { + #region Fields + + private static readonly List Categorys = new List() + { + new Category() + { + Id = 1, Name = "First" + } + }; + private long _currentId; + + #endregion + + #region Ctors + + public InMemoryCategoriesService() + { + _currentId = Categorys.Count(); + } + #endregion + + #region Methods + public async Task> GetAllAsync(CancellationToken ct) + { + await Task.Delay(3000, ct); + return await Task.FromResult>(Categorys.AsReadOnly()); + } + + public async Task GetByIdAsync(long id, CancellationToken ct) + { + await Task.Delay(3000, ct); + return await Task.FromResult(Categorys.SingleOrDefault(g => g.Id == id)); + } + + public async Task UpdateAsync(Category Category, CancellationToken ct) + { + await Task.Delay(5000, ct); + var toUpdate = Categorys.SingleOrDefault(g => g.Id == Category.Id); + if (toUpdate == null) + return null; + + toUpdate.Name = Category.Name; + + return await Task.FromResult(toUpdate); + } + + public async Task AddAsync(Category Category, CancellationToken ct) + { + await Task.Delay(3000, ct); + Category.Id = ++_currentId; + Categorys.Add(Category); + return await Task.FromResult(Category); + } + + public Task RemoveAsync(long id, CancellationToken ct) + { + throw new System.NotImplementedException(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/InMemoryCertificatesService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/InMemoryCertificatesService.cs index 4e3df4b5..fb7cb21b 100644 --- a/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/InMemoryCertificatesService.cs +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Impl/Services/InMemoryCertificatesService.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; +using O2NextGen.CertificateManagement.Business.Models; +using O2NextGen.CertificateManagement.Business.Services; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using O2NextGen.CertificateManagement.Business.Models; -using O2NextGen.CertificateManagement.Business.Services; namespace O2NextGen.CertificateManagement.Impl.Services { diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/CGenDbContext.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/CGenDbContext.cs new file mode 100644 index 00000000..1ff461c5 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/CGenDbContext.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Infrastructure.Data +{ + public class CGenDbContext : DbContext + { + + #region Fields + + public DbSet Certificates { get; set; } + public DbSet Categories { get; set; } + #endregion + + #region Ctors + + public CGenDbContext(DbContextOptions options) + : base(options) + { + } + + #endregion + + #region Configure + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); + } + + //protected override void OnModelCreating(ModelBuilder modelBuilder) + //{ + // modelBuilder.Entity(ConfigureCertificateEntity); + //} + + //private void ConfigureCertificateEntity(EntityTypeBuilder builder) + //{ + // builder.ToTable("Certificate"); + + // builder.Property(ci => ci.Id) + // .HasColumnType("bigint") + // .UseHiLo("certificate_hilo") + // .IsRequired(); + //} + + #endregion + } +} \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/CategoryDbEntityConfiguration.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/CategoryDbEntityConfiguration.cs new file mode 100644 index 00000000..5880296c --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/CategoryDbEntityConfiguration.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Infrastructure.Data.Configurations +{ + + public class CategoryDbEntityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Category"); + + builder.Property(ci => ci.Id) + .HasColumnType("bigint") + .UseHiLo("category_hilo") + .IsRequired(); + + //builder.Property(ci => ci.ExternalId) + // .IsRequired(); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/CertificateDbEntityConfiguration.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/CertificateDbEntityConfiguration.cs new file mode 100644 index 00000000..1fef5dec --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/CertificateDbEntityConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Infrastructure.Data.Configurations +{ + + public class CertificateDbEntityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Certificate"); + + builder.Property(ci => ci.Id) + .HasColumnType("bigint") + .UseHiLo("certificate_hilo") + .IsRequired(); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/LanguageInfoDbEntityConfiguration.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/LanguageInfoDbEntityConfiguration.cs new file mode 100644 index 00000000..62e2c245 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Configurations/LanguageInfoDbEntityConfiguration.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using O2NextGen.CertificateManagement.Domain.Entities; + +namespace O2NextGen.CertificateManagement.Infrastructure.Data.Configurations +{ + public class LanguageInfoDbEntityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("LanguageInfos") + .HasOne(e => e.Certificate) + .WithMany(e => e.LanguageInfos) + .HasForeignKey(e => e.CertificateId) + .OnDelete(DeleteBehavior.Cascade); + + builder.Property(ci => ci.Id) + .HasColumnType("bigint") + .UseHiLo("languageInfo_hilo") + .IsRequired(); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/EfRepository.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/EfRepository.cs new file mode 100644 index 00000000..499a4ba7 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/EfRepository.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using O2NextGen.CertificateManagement.Domain.Data; + +namespace O2NextGen.CertificateManagement.Infrastructure.Data +{ + + public class EfRepository : IRepository where T : class + { + private readonly CGenDbContext _dbContext; + + public EfRepository(CGenDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task AddAsync(T entity, CancellationToken ct) + { + await _dbContext.Set().AddAsync(entity, ct); + await _dbContext.SaveChangesAsync(ct); + return entity; + } + + public async Task UpdateAsync(T entity, CancellationToken ct) + { + var entry = _dbContext.Entry(entity); + //if (entity is IVersionedEntity versionedEntity) + //{ + // entry.OriginalValues[nameof(IVersionedEntity.RowVersion)] = versionedEntity.RowVersion; + //} + entry.State = EntityState.Modified; + await _dbContext.SaveChangesAsync(ct); + } + + public async Task DeleteAsync(T entity, CancellationToken ct) + { + _dbContext.Set().Remove(entity); + await _dbContext.SaveChangesAsync(ct); + } + } + +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/20221225101744_InitDatabase.Designer.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/20221225101744_InitDatabase.Designer.cs new file mode 100644 index 00000000..61e13644 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/20221225101744_InitDatabase.Designer.cs @@ -0,0 +1,216 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using O2NextGen.CertificateManagement.Infrastructure.Data; + +#nullable disable + +namespace O2NextGen.CertificateManagement.Infrastructure.Data.Migrations +{ + [DbContext(typeof(CGenDbContext))] + [Migration("20221225101744_InitDatabase")] + partial class InitDatabase + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.HasSequence("category_hilo") + .IncrementsBy(10); + + modelBuilder.HasSequence("certificate_hilo") + .IncrementsBy(10); + + modelBuilder.HasSequence("languageInfo_hilo") + .IncrementsBy(10); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CategoryDbEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "category_hilo"); + + b.Property("AddedDate") + .HasColumnType("bigint"); + + b.Property("CategoryDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("CategoryName") + .HasColumnType("nvarchar(max)"); + + b.Property("CategorySeries") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedDate") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("bigint"); + + b.Property("QuantityCertificates") + .HasColumnType("int"); + + b.Property("QuantityPublishCode") + .HasColumnType("int"); + + b.Property("TimeLifeInDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Category", (string)null); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "certificate_hilo"); + + b.Property("AddedDate") + .HasColumnType("bigint"); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("CreatorId") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedDate") + .HasColumnType("bigint"); + + b.Property("ExpiredDate") + .HasColumnType("bigint"); + + b.Property("ExternalId") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsVisible") + .HasColumnType("bit"); + + b.Property("Lock") + .HasColumnType("bit"); + + b.Property("LockInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("LockedDate") + .HasColumnType("bigint"); + + b.Property("ModifiedDate") + .HasColumnType("bigint"); + + b.Property("OwnerAccountId") + .HasColumnType("nvarchar(max)"); + + b.Property("PublishCode") + .HasColumnType("nvarchar(max)"); + + b.Property("PublishDate") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Certificate", (string)null); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.LanguageInfoDbEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "languageInfo_hilo"); + + b.Property("AddedDate") + .HasColumnType("bigint"); + + b.Property("CertificateId") + .HasColumnType("bigint"); + + b.Property("DeletedDate") + .HasColumnType("bigint"); + + b.Property("Firstname") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LanguageId") + .HasColumnType("int"); + + b.Property("Lastname") + .HasColumnType("nvarchar(max)"); + + b.Property("Middlename") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedDate") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CertificateId"); + + b.ToTable("LanguageInfos", (string)null); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", b => + { + b.HasOne("O2NextGen.CertificateManagement.Domain.Entities.CategoryDbEntity", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.LanguageInfoDbEntity", b => + { + b.HasOne("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", "Certificate") + .WithMany("LanguageInfos") + .HasForeignKey("CertificateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Certificate"); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", b => + { + b.Navigation("LanguageInfos"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/20221225101744_InitDatabase.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/20221225101744_InitDatabase.cs new file mode 100644 index 00000000..b9c826db --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/20221225101744_InitDatabase.cs @@ -0,0 +1,136 @@ +#nullable disable + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace O2NextGen.CertificateManagement.Infrastructure.Data.Migrations +{ + public partial class InitDatabase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "category_hilo", + incrementBy: 10); + + migrationBuilder.CreateSequence( + name: "certificate_hilo", + incrementBy: 10); + + migrationBuilder.CreateSequence( + name: "languageInfo_hilo", + incrementBy: 10); + + migrationBuilder.CreateTable( + name: "Category", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + ModifiedDate = table.Column(type: "bigint", nullable: false), + AddedDate = table.Column(type: "bigint", nullable: false), + DeletedDate = table.Column(type: "bigint", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: true), + CustomerId = table.Column(type: "nvarchar(max)", nullable: true), + CategorySeries = table.Column(type: "nvarchar(max)", nullable: true), + QuantityCertificates = table.Column(type: "int", nullable: false), + QuantityPublishCode = table.Column(type: "int", nullable: false), + CategoryName = table.Column(type: "nvarchar(max)", nullable: true), + CategoryDescription = table.Column(type: "nvarchar(max)", nullable: true), + TimeLifeInDays = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Category", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Certificate", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + ExternalId = table.Column(type: "nvarchar(max)", nullable: true), + ModifiedDate = table.Column(type: "bigint", nullable: false), + AddedDate = table.Column(type: "bigint", nullable: false), + DeletedDate = table.Column(type: "bigint", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: true), + OwnerAccountId = table.Column(type: "nvarchar(max)", nullable: true), + CustomerId = table.Column(type: "nvarchar(max)", nullable: true), + ExpiredDate = table.Column(type: "bigint", nullable: false), + PublishDate = table.Column(type: "bigint", nullable: false), + CreatorId = table.Column(type: "nvarchar(max)", nullable: true), + PublishCode = table.Column(type: "nvarchar(max)", nullable: true), + IsVisible = table.Column(type: "bit", nullable: false), + CategoryId = table.Column(type: "bigint", nullable: false), + Lock = table.Column(type: "bit", nullable: false), + LockedDate = table.Column(type: "bigint", nullable: false), + LockInfo = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Certificate", x => x.Id); + table.ForeignKey( + name: "FK_Certificate_Category_CategoryId", + column: x => x.CategoryId, + principalTable: "Category", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "LanguageInfos", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + ModifiedDate = table.Column(type: "bigint", nullable: false), + AddedDate = table.Column(type: "bigint", nullable: false), + DeletedDate = table.Column(type: "bigint", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: true), + CertificateId = table.Column(type: "bigint", nullable: false), + LanguageId = table.Column(type: "int", nullable: false), + Lastname = table.Column(type: "nvarchar(max)", nullable: true), + Firstname = table.Column(type: "nvarchar(max)", nullable: true), + Middlename = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_LanguageInfos", x => x.Id); + table.ForeignKey( + name: "FK_LanguageInfos_Certificate_CertificateId", + column: x => x.CertificateId, + principalTable: "Certificate", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Certificate_CategoryId", + table: "Certificate", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_LanguageInfos_CertificateId", + table: "LanguageInfos", + column: "CertificateId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LanguageInfos"); + + migrationBuilder.DropTable( + name: "Certificate"); + + migrationBuilder.DropTable( + name: "Category"); + + migrationBuilder.DropSequence( + name: "category_hilo"); + + migrationBuilder.DropSequence( + name: "certificate_hilo"); + + migrationBuilder.DropSequence( + name: "languageInfo_hilo"); + } + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/CGenDbContextModelSnapshot.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/CGenDbContextModelSnapshot.cs new file mode 100644 index 00000000..5d7ca843 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Data/Migrations/CGenDbContextModelSnapshot.cs @@ -0,0 +1,210 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +#nullable disable + +namespace O2NextGen.CertificateManagement.Infrastructure.Data.Migrations +{ + [DbContext(typeof(CGenDbContext))] + partial class CGenDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.HasSequence("category_hilo") + .IncrementsBy(10); + + modelBuilder.HasSequence("certificate_hilo") + .IncrementsBy(10); + + modelBuilder.HasSequence("languageInfo_hilo") + .IncrementsBy(10); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CategoryDbEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "category_hilo"); + + b.Property("AddedDate") + .HasColumnType("bigint"); + + b.Property("CategoryDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("CategoryName") + .HasColumnType("nvarchar(max)"); + + b.Property("CategorySeries") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedDate") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ModifiedDate") + .HasColumnType("bigint"); + + b.Property("QuantityCertificates") + .HasColumnType("int"); + + b.Property("QuantityPublishCode") + .HasColumnType("int"); + + b.Property("TimeLifeInDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Category", (string)null); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "certificate_hilo"); + + b.Property("AddedDate") + .HasColumnType("bigint"); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("CreatorId") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedDate") + .HasColumnType("bigint"); + + b.Property("ExpiredDate") + .HasColumnType("bigint"); + + b.Property("ExternalId") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsVisible") + .HasColumnType("bit"); + + b.Property("Lock") + .HasColumnType("bit"); + + b.Property("LockInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("LockedDate") + .HasColumnType("bigint"); + + b.Property("ModifiedDate") + .HasColumnType("bigint"); + + b.Property("OwnerAccountId") + .HasColumnType("nvarchar(max)"); + + b.Property("PublishCode") + .HasColumnType("nvarchar(max)"); + + b.Property("PublishDate") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Certificate", (string)null); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.LanguageInfoDbEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseHiLo(b.Property("Id"), "languageInfo_hilo"); + + b.Property("AddedDate") + .HasColumnType("bigint"); + + b.Property("CertificateId") + .HasColumnType("bigint"); + + b.Property("DeletedDate") + .HasColumnType("bigint"); + + b.Property("Firstname") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LanguageId") + .HasColumnType("int"); + + b.Property("Lastname") + .HasColumnType("nvarchar(max)"); + + b.Property("Middlename") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedDate") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CertificateId"); + + b.ToTable("LanguageInfos", (string)null); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", b => + { + b.HasOne("O2NextGen.CertificateManagement.Domain.Entities.CategoryDbEntity", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.LanguageInfoDbEntity", b => + { + b.HasOne("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", "Certificate") + .WithMany("LanguageInfos") + .HasForeignKey("CertificateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Certificate"); + }); + + modelBuilder.Entity("O2NextGen.CertificateManagement.Domain.Entities.CertificateDbEntity", b => + { + b.Navigation("LanguageInfos"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.csproj new file mode 100644 index 00000000..4f43ce1d --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + + + + + + + + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.v3.ncrunchproject new file mode 100644 index 00000000..44cdd50f --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/O2NextGen.CertificateManagement.Infrastructure.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + True + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CategoriesQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CategoriesQueryHandler.cs new file mode 100644 index 00000000..5b2c2ef5 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CategoriesQueryHandler.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Infrastructure.Data; + +namespace O2NextGen.CertificateManagement.Infrastructure.Queries +{ + public class CategoriesQueryHandler : IQueryHandler> + { + private readonly CGenDbContext context; + + public CategoriesQueryHandler(CGenDbContext context) + { + this.context = context; + } + public async Task> HandleAsync(CategoriesQuery query, CancellationToken ct) + => (await context + .Categories + .ToListAsync(ct)) + .AsReadOnly(); + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CategoryQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CategoryQueryHandler.cs new file mode 100644 index 00000000..4bfa8ee7 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CategoryQueryHandler.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Infrastructure.Data; + +namespace O2NextGen.CertificateManagement.Infrastructure.Queries +{ + public class CategoryQueryHandler : IQueryHandler + { + private readonly CGenDbContext context; + + public CategoryQueryHandler(CGenDbContext context) + { + this.context = context; + } + public async Task HandleAsync(CategoryQuery query, CancellationToken ct) + { + var result = await context.Set().FindAsync(new object[] { query.Id }, ct); + return result; + } + + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CertificateQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CertificateQueryHandler.cs new file mode 100644 index 00000000..31e2804c --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CertificateQueryHandler.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Infrastructure.Data; + +namespace O2NextGen.CertificateManagement.Infrastructure.Queries +{ + public class CertificateQueryHandler : IQueryHandler + { + private readonly CGenDbContext context; + + public CertificateQueryHandler(CGenDbContext context) + { + this.context = context; + } + public async Task HandleAsync(CertificateQuery query, CancellationToken ct) + { + var result = await context.Set().FindAsync(new object[] { query.Id }, ct); + return result; + } + + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CertificatesQueryHandler.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CertificatesQueryHandler.cs new file mode 100644 index 00000000..1c9c8a1e --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.Infrastructure/Queries/CertificatesQueryHandler.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Domain.Data.Queries; +using O2NextGen.CertificateManagement.Domain.Entities; +using O2NextGen.CertificateManagement.Infrastructure.Data; + +namespace O2NextGen.CertificateManagement.Infrastructure.Queries +{ + public class CertificatesQueryHandler : IQueryHandler> + { + private readonly CGenDbContext context; + + public CertificatesQueryHandler(CGenDbContext context) + { + this.context = context; + } + public async Task> HandleAsync(CertificatesQuery query, CancellationToken ct) + => (await context + .Certificates + .ToListAsync(ct)) + .AsReadOnly(); + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerHostedService.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerHostedService.cs new file mode 100644 index 00000000..7219b28e --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerHostedService.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace O2NextGen.CertificateManagement.StartupTasks.DatabaseInitializer +{ + public class DatabaseInitializerHostedService : IHostedService + where TDbContext : DbContext + { + private readonly IServiceScopeFactory _scopeFactory; + + private readonly DatabaseInitializerSettings _settings; + + public DatabaseInitializerHostedService(IServiceScopeFactory scopeFactory, DatabaseInitializerSettings settings) + { + _scopeFactory = scopeFactory; + _settings = settings; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var scope = _scopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + if (_settings.Initialize) + { + await dbContext.Database.MigrateAsync(cancellationToken); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerServiceCollectionExtensions.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerServiceCollectionExtensions.cs new file mode 100644 index 00000000..e7d2751d --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace O2NextGen.CertificateManagement.StartupTasks.DatabaseInitializer +{ + public static class DatabaseInitializerServiceCollectionExtensions + { + public static IServiceCollection AddDatabaseInitializer( + this IServiceCollection services, + string databaseInitializerSettingsPath = nameof(DatabaseInitializerSettings)) + where TDbContext : DbContext + { + services.AddSingleton(serviceProvider + => serviceProvider + .GetRequiredService() + .GetSection(databaseInitializerSettingsPath) + .Get()); + + return services.AddHostedService>(); + } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerSettings.cs b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerSettings.cs new file mode 100644 index 00000000..f27b3447 --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/DatabaseInitializer/DatabaseInitializerSettings.cs @@ -0,0 +1,8 @@ +namespace O2NextGen.CertificateManagement.StartupTasks.DatabaseInitializer +{ + public class DatabaseInitializerSettings + { + public bool Initialize { get; set; } + } +} + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.csproj b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.csproj new file mode 100644 index 00000000..ea6f48dd --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.v3.ncrunchproject b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.v3.ncrunchproject new file mode 100644 index 00000000..44cdd50f --- /dev/null +++ b/src/Services/c-gen/O2NextGen.CertificateManagement.StartupTasks/O2NextGen.CertificateManagement.StartupTasks.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + True + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryScenarioBase.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryScenarioBase.cs new file mode 100644 index 00000000..7a0fff26 --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryScenarioBase.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using System.IO; +using System.Reflection; + +namespace IntegrationTests.O2NextGen.CertificateManagement.Api +{ + public class CategoryScenarioBase + { + public TestServer CreateServer() + { + var path = Assembly.GetAssembly(typeof(CertificateScenarioBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + // cb.AddJsonFile("appsettings.json", false) + // .AddEnvironmentVariables(); + }).UseStartup(); + + var testServer = new TestServer(hostBuilder); + + return testServer; + } + } +} + diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryServiceTests.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryServiceTests.cs new file mode 100644 index 00000000..a54b8d0a --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryServiceTests.cs @@ -0,0 +1,8 @@ +namespace IntegrationTests.O2NextGen.CertificateManagement.Api +{ + public class CategoryServiceTests : CategoryScenarioBase + { + + } +} + diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateManagementTestsStartup.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateManagementTestsStartup.cs index 1a97bb54..a48e257d 100644 --- a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateManagementTestsStartup.cs +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateManagementTestsStartup.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; -using O2NextGen.CertificateManagement.Api; namespace IntegrationTests.O2NextGen.CertificateManagement.Api { - public class CertificateManagementTestsStartup : Startup + public class TestsStartup //: Startup { - public CertificateManagementTestsStartup(IConfiguration config,IHostingEnvironment env) + public TestsStartup(IConfiguration config, IWebHostEnvironment env) : base(config,env) { } diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateScenarioBase.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateScenarioBase.cs index 13c98c59..0a4bd3e0 100644 --- a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateScenarioBase.cs +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateScenarioBase.cs @@ -18,7 +18,7 @@ public TestServer CreateServer() { // cb.AddJsonFile("appsettings.json", false) // .AddEnvironmentVariables(); - }).UseStartup(); + }).UseStartup(); var testServer = new TestServer(hostBuilder); @@ -48,10 +48,15 @@ public static string CertificateBy(int id) } } - //public static class Put - //{ - // public static string CancelOrder = "api/v1/orders/cancel"; - // public static string ShipOrder = "api/v1/orders/ship"; - //} + public static class Put + { + // public static string CancelOrder = "api/v1/orders/cancel"; + // public static string ShipOrder = "api/v1/orders/ship"; + } + + public static class Post + { + + } } } \ No newline at end of file diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateServiceTests.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateServiceTests.cs index 546b827e..0ba0d02e 100644 --- a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateServiceTests.cs +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateServiceTests.cs @@ -44,7 +44,7 @@ public class CertificateServiceTests : CertificateScenarioBase // } [Fact] - public async Task Get_get_catalogitem_by_id_and_response_not_found_status_code() + public async Task Get_certificate_item_by_id_and_response_not_found_status_code() { using (var server = CreateServer()) { diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.csproj b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.csproj index f00cd14e..e377fa4e 100644 --- a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.csproj +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.csproj @@ -1,15 +1,13 @@ - netcoreapp2.2 + net6.0 false - - @@ -17,10 +15,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/CertificateControllerTests.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/CertificateControllerTests.cs new file mode 100644 index 00000000..4496bff2 --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/CertificateControllerTests.cs @@ -0,0 +1,31 @@ +using System; + +namespace IntegrationTests.O2NextGen.CertificateManagement.Application +{ + public class CertificateControllerTests + { + [Fact] + public void CertificateController_GetByIdAsync_Test() + { + throw new NotImplementedException(); + } + + [Fact] + public void CertificateController_GetAllAsync_Test() + { + throw new NotImplementedException(); + } + + [Fact] + public void CertificateController_UpdateAsync_Test() + { + throw new NotImplementedException(); + } + + [Fact] + public void CertificateController_AddAsync_Test() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/CertificateScenarioBase.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/CertificateScenarioBase.cs new file mode 100644 index 00000000..4219f56e --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/CertificateScenarioBase.cs @@ -0,0 +1,48 @@ +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; + +namespace IntegrationTests.O2NextGen.CertificateManagement.Application +{ + public class CertificateScenarioBase + { + public TestServer CreateServer() + { + var path = Assembly.GetAssembly(typeof(CertificateScenarioBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + // cb.AddJsonFile("appsettings.json", false) + // .AddEnvironmentVariables(); + }).UseStartup(); + + var testServer = new TestServer(hostBuilder); + return testServer; + } + + public static class Get + { + public static string Certificates = "certificates"; + + public static string CertificateBy(int id) + { + return $"certificates/{id}"; + } + } + + public static class Put + { + // public static string CancelOrder = "api/v1/orders/cancel"; + // public static string ShipOrder = "api/v1/orders/ship"; + } + + public static class Post + { + + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.csproj b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.csproj new file mode 100644 index 00000000..667c8936 --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.v3.ncrunchproject b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/IntegrationTests.O2NextGen.CertificateManagement.Application.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/TestsStartup.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/TestsStartup.cs new file mode 100644 index 00000000..b41608fb --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/TestsStartup.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using O2NextGen.CertificateManagement.Api; + +namespace IntegrationTests.O2NextGen.CertificateManagement.Application +{ + public class TestsStartup //: Startup + { + // public TestsStartup(IConfiguration config, IWebHostEnvironment env) + // : base(config, env) + //{ + //} + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/Usings.cs b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/Usings.cs new file mode 100644 index 00000000..9df1d421 --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/appsettings.json b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/appsettings.json new file mode 100644 index 00000000..a88e216b --- /dev/null +++ b/src/Services/c-gen/Tests/IntegrationTests.O2NextGen.CertificateManagement.Application/appsettings.json @@ -0,0 +1,18 @@ +{ + "DatabaseInitializerSettings": { + "Initialize": true + }, + "IsTestMode": "false", + "ConnectionString": "Server=20.112.192.201;Initial Catalog=O2NextGen.CertificateDb;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30;", + "Urls": { + "Auth": "http://localhost:10001" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} + diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryScenarioBase.cs b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryScenarioBase.cs new file mode 100644 index 00000000..568f435d --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryScenarioBase.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using System.IO; +using System.Reflection; + +namespace IntegrationTests.O2NextGen.CertificateManagement.Api; + +public class CategoryScenarioBase +{ + public TestServer CreateServer() + { + var path = Assembly.GetAssembly(typeof(CertificateScenarioBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + // cb.AddJsonFile("appsettings.json", false) + // .AddEnvironmentVariables(); + }).UseStartup(); + + var testServer = new TestServer(hostBuilder); + + return testServer; + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryServiceTests.cs b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryServiceTests.cs new file mode 100644 index 00000000..c53e99a7 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CategoryServiceTests.cs @@ -0,0 +1,6 @@ +namespace IntegrationTests.O2NextGen.CertificateManagement.Api; + +public class CategoryServiceTests : CategoryScenarioBase +{ + +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateManagementTestsStartup.cs b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateManagementTestsStartup.cs new file mode 100644 index 00000000..6d7287b5 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateManagementTestsStartup.cs @@ -0,0 +1,18 @@ +namespace IntegrationTests.O2NextGen.CertificateManagement.Api; + +public class TestsStartup //: Startup +{ + // public TestsStartup(IConfiguration config, IWebHostEnvironment env) + // : base(config,env) + // { + // } + + // protected override void ConfigureAuth(IApplicationBuilder app) + // { + // //if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + // // app.UseMiddleware(); + // //else + // app.UseMiddleware(); + // base.ConfigureAuth(app); + // } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateScenarioBase.cs b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateScenarioBase.cs new file mode 100644 index 00000000..11b14e41 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateScenarioBase.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; + +namespace IntegrationTests.O2NextGen.CertificateManagement.Api; + +public class CertificateScenarioBase +{ + public TestServer CreateServer() + { + var path = Assembly.GetAssembly(typeof(CertificateScenarioBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + // cb.AddJsonFile("appsettings.json", false) + // .AddEnvironmentVariables(); + }).UseStartup(); + + var testServer = new TestServer(hostBuilder); + + //testServer.Host + //.MigrateDbContext((context, services) => + //{ + // var env = services.GetService(); + // var settings = services.GetService>(); + // var logger = services.GetService>(); + + // //new CertificateDataContext() + // // .SeedAsync(context, env, settings, logger) + // // .Wait(); + //}) + //.MigrateDbContext((_, __) => { }); + + return testServer; + } + + public static class Get + { + public static string Certificates = "certificates"; + + public static string CertificateBy(int id) + { + return $"certificates/{id}"; + } + } + + public static class Put + { + // public static string CancelOrder = "api/v1/orders/cancel"; + // public static string ShipOrder = "api/v1/orders/ship"; + } + + public static class Post + { + + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateServiceTests.cs b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateServiceTests.cs new file mode 100644 index 00000000..a54fa2e8 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/CertificateServiceTests.cs @@ -0,0 +1,58 @@ +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace IntegrationTests.O2NextGen.CertificateManagement.Api; + +public class CertificateServiceTests : CertificateScenarioBase +{ + + // [Fact] + // public async Task Get_get_all_stored_orders_and_response_ok_status_code() + // { + // using (var server = CreateServer()) + // { + // var response = await server.CreateClient() + // .GetAsync(Get.Certificates); + // + // response.EnsureSuccessStatusCode(); + // } + // } + // + // [Fact] + // public async Task Get_get_catalogitem_by_id_and_response_ok_status_code() + // { + // using (var server = CreateServer()) + // { + // var response = await server.CreateClient() + // .GetAsync(Get.CertificateBy(1)); + // + // response.EnsureSuccessStatusCode(); + // } + // } + // + // [Fact] + // public async Task Get_get_catalogitem_by_id_and_response_bad_request_status_code() + // { + // using (var server = CreateServer()) + // { + // var response = await server.CreateClient() + // .GetAsync(Get.CertificateBy(1)); + // + // Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + // } + // } + + [Fact] + public async Task Get_certificate_item_by_id_and_response_not_found_status_code() + { + + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.CertificateBy(int.MaxValue)); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.csproj b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.csproj new file mode 100644 index 00000000..e377fa4e --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.csproj @@ -0,0 +1,38 @@ + + + + net6.0 + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + true + PreserveNewest + PreserveNewest + + + diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.sln b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.sln new file mode 100644 index 00000000..c7ca75ad --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 25.0.1704.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests.O2NextGen.CertificateManagement.Api", "IntegrationTests.O2NextGen.CertificateManagement.Api.csproj", "{2048912F-DF3D-464A-8239-DA6C99662DB5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2048912F-DF3D-464A-8239-DA6C99662DB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2048912F-DF3D-464A-8239-DA6C99662DB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2048912F-DF3D-464A-8239-DA6C99662DB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2048912F-DF3D-464A-8239-DA6C99662DB5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BB4B76CA-581F-44C3-A268-B1EB8FEA16D8} + EndGlobalSection +EndGlobal diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/IntegrationTests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/appsettings.json b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/appsettings.json new file mode 100644 index 00000000..ef9f68fa --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/IntegrationTests.O2NextGen.CertificateManagement.Api/appsettings.json @@ -0,0 +1,15 @@ +{ + "ConnectionString": "Server=localhost;Initial Catalog=IntegrationTests.O2NextGen.CertificateDb;Persist Security Info=False;User ID=sa;Password=your@Password;Connection Timeout=30;", + "Urls": { + "Auth": "http://localhost:10001" + }, + "AllowedHosts": "*", + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.csproj b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.csproj new file mode 100644 index 00000000..a91b6ea1 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.csproj b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.csproj new file mode 100644 index 00000000..e899dc0c --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.v3.ncrunchproject b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/Skeleton/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.csproj b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.csproj index 263a4732..a91b6ea1 100644 --- a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.csproj +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.csproj @@ -1,16 +1,16 @@ - netcoreapp2.2 + net6.0 false - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Api/Tests.O2NextGen.CertificateManagement.Api.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseConfigTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseConfigTests.cs new file mode 100644 index 00000000..d9df0d40 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseConfigTests.cs @@ -0,0 +1,6 @@ +namespace Tests.O2NextGen.CertificateManagement.Application.Base; + +public class BaseConfigTests : BaseTests + where TClass : class +{ +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseControllerTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseControllerTests.cs new file mode 100644 index 00000000..168fef05 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseControllerTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace Tests.O2NextGen.CertificateManagement.Application.Base; + +public class BaseControllerTests : BaseTests + where TClass : class +{ + + [Fact] + public void ControllerBaseTests_AttributeApiController() + { + Assert.NotNull(Attribute.GetCustomAttribute(typeof(TClass), typeof(ApiControllerAttribute))); + } + + [Fact] + public void ControllerBaseTests_AttributeRoute() + { + Assert.NotNull(Attribute.GetCustomAttribute(typeof(TClass), typeof(RouteAttribute))); + } + + [Fact] + public virtual void ControllerBaseTests_AttributeAuthorize() + { + Assert.NotNull(Attribute.GetCustomAttribute(typeof(TClass), typeof(AuthorizeAttribute))); + } + + [Fact] + public virtual void ControllerBaseTests_AttributeApiVersion() + { + Assert.NotNull(Attribute.GetCustomAttributes(typeof(TClass), typeof(ApiVersionAttribute))); + } + + [Fact] + public void ControllerBaseTests_BaseClass() + { + Assert.True(typeof(ControllerBase).IsAssignableFrom(typeof(TClass))); + } + + [Fact] + public void ControllerBaseTests_NameContainController() + { + Assert.Contains("Controller", typeof(TClass).Name); + } + + [Fact] + public void Methods_MarkerAttributePostOrGetOrDelete() + { + var result = typeof(TClass) + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Count(p => + CustomAttributeExtensions.GetCustomAttribute((MemberInfo) p, typeof(HttpGetAttribute), false) == null + && + CustomAttributeExtensions.GetCustomAttribute((MemberInfo) p, typeof(HttpPostAttribute), false) == null + && + CustomAttributeExtensions.GetCustomAttribute((MemberInfo) p, typeof(HttpDeleteAttribute), false) == null + && + CustomAttributeExtensions.GetCustomAttribute((MemberInfo) p, typeof(HttpPutAttribute), false) == null + ); + Assert.True(result == 0); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseHelper.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseHelper.cs new file mode 100644 index 00000000..afdeff6c --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; + +namespace Tests.O2NextGen.CertificateManagement.Application.Base; + +public static class BaseHelper +{ + public static bool It_CheckExistProperty(string nameProperty) + { + return typeof(TClass) + .GetProperties() + .SingleOrDefault(p => p.Name == nameProperty) != null; + } + + public static bool It_CheckExistPropertyOfType(string nameProperty, Type type) + { + return type == + typeof(TClass).GetProperties().SingleOrDefault(p => p.Name == nameProperty)?.PropertyType; + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseTests.cs new file mode 100644 index 00000000..65140d2f --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Base/BaseTests.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace Tests.O2NextGen.CertificateManagement.Application.Base; + +public class BaseTests + where TClass : class +{ + public virtual void It_CheckClassName(string name = "") + { + Assert.Equal(name, typeof(TClass).Name); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/CreateCategoryModelTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/CreateCategoryModelTests.cs new file mode 100644 index 00000000..3ed8cb4c --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/CreateCategoryModelTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using Moq; +using O2NextGen.CertificateManagement.Application.Features.Categories; +using Xunit; + +namespace Tests.O2NextGen.CertificateManagement.Application; + +public class CreateCategoryModelTests +{ + #region Tests of CategoryName Property + + [Fact] + public void CreateCategoryModel_CheckPropertySetCategoryName_Test() + { + // Arrange + var mock = new Mock(); + // Act + mock.SetupProperty(m => m.CategoryName, "CategoryName"); + // Assert + mock.Object.CategoryName.Should().Be("CategoryName"); + } + + #endregion + + #region Tests of CategoryDescription Property + + [Fact] + public void CreateCategoryModel_CheckPropertySetCategoryDescription_Test() + { + // Arrange + var mock = new Mock(); + // Act + mock.SetupProperty(m => m.CategoryDescription, "CategoryDescription"); + // Assert + mock.Object.CategoryDescription.Should().Be("CategoryDescription"); + } + + #endregion + + #region Tests of CategorySeries Property + + [Fact] + public void CreateCategoryModel_CheckPropertySetCategorySeries_Test() + { + // Arrange + var mock = new Mock(); + // Act + mock.SetupProperty(m => m.CategorySeries, "CategorySeries"); + // Assert + mock.Object.CategorySeries.Should().Be("CategorySeries"); + } + + #endregion + + #region Tests of CustomerId Property + + [Fact] + public void CreateCategoryModel_CheckPropertySetCustomerId_Test() + { + // Arrange + var mock = new Mock(); + // Act + mock.SetupProperty(m => m.CustomerId, "CustomerId"); + // Assert + mock.Object.CustomerId.Should().Be("CustomerId"); + } + + #endregion +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Features/Categories/CategoryControllerTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Features/Categories/CategoryControllerTests.cs new file mode 100644 index 00000000..f6ace8cc --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Features/Categories/CategoryControllerTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using O2NextGen.CertificateManagement.Application.Features.Categories; +using O2NextGen.CertificateManagement.Domain.UseCases.ForCategory.GetCategory; +using Tests.O2NextGen.CertificateManagement.Application.Base; +using Xunit; + +namespace Tests.O2NextGen.CertificateManagement.Application.Features.Categories; + +public class CategoryControllerTests + : BaseControllerTests +{ + #region Tests for Attributes + + [Theory] + [InlineData("CategoriesController")] + public override void It_CheckClassName(string name = "") + { + base.It_CheckClassName(name); + } + + [Fact] + public void CategoryControllerTests_AttributeApiVersion_Supported_v1_0() + { + Assert.Contains(Attribute.GetCustomAttributes(typeof(CategoriesController), typeof(ApiVersionAttribute)), att => + ((ApiVersionAttribute) att).Versions.Any(x => x is {MajorVersion: 1, MinorVersion: 0})); + } + + [Fact] + public void CategoryControllerTests_AttributeApiVersion_Supported_v1_1() + { + Assert.Contains(Attribute.GetCustomAttributes(typeof(CategoriesController), typeof(ApiVersionAttribute)), att => + ((ApiVersionAttribute) att).Versions.Any(x => x is {MajorVersion: 1, MinorVersion: 1})); + } + + #endregion + + #region Tests for Logging Methods + + [Fact] + public async Task CategoryControllerTests_GetById_LoggerInfo_Tests() + { + //Arrange + var mediator = new Mock(); + + CancellationTokenSource cts = new(); + + var cancellationToken = cts.Token; + + var categoryQueryResult = new Mock(); + + mediator.Setup(_ => _.Send(It.IsAny(), cancellationToken)) + .Returns(() => Task.FromResult( + It.IsAny() + )); + + var logger = new Mock>(); + + // Act + var controller = new CategoriesController(mediator.Object, logger.Object); + const long id = 1; + var result = await controller.GetByIdAsync(id, cancellationToken); + + // Asser + logger.VerifyLog(_ => _.LogInformation(It.IsAny())); + logger.VerifyLog(_ => _.LogInformation(It.IsNotNull())); + logger.VerifyLog(_ => _.LogInformation(It.Is(msg => msg.Length > 0))); + logger.VerifyLog(_ => _.LogInformation("Call API method {ByIdAsyncName}: id = {Id}", "GetByIdAsync", id)); + } + + [Fact] + public async Task CategoryControllerTests_GetById_Ok_LoggerInfo_Tests() + { + //Arrange + var mediator = new Mock(); + + CancellationTokenSource cts = new(); + + var cancellationToken = cts.Token; + + var categoryQueryResult = new Mock(); + + mediator.Setup(_ => _.Send(It.IsAny(), cancellationToken)) + .Returns(() => Task.FromResult( + It.IsAny() + )); + + var logger = new Mock>(); + + // Act + var controller = new CategoriesController(mediator.Object, logger.Object); + const long id = 1; + var result = await controller.GetByIdAsync(id, cancellationToken); + + // Asser + logger.VerifyLog(_ => _.LogInformation(It.IsAny())); + logger.VerifyLog(_ => _.LogInformation(It.IsNotNull())); + logger.VerifyLog(_ => _.LogInformation(It.Is(msg => msg.Length > 0))); + logger.VerifyLog(_ => _.LogInformation("GetByIdAsync: OK")); + } + + [Fact] + public async Task CategoryControllerTests_GetById_LoggerError_Tests() + { + //Arrange + var mediator = new Mock(); + CancellationTokenSource cts = new(); + CancellationToken cancellationToken = cts.Token; + + var categoryQueryResult = new Mock(); + mediator.Setup(_ => _.Send(It.IsAny(), cancellationToken)) + .Returns(() => Task.FromResult( + null as GetCategoryQueryResult + )); + // .Returns(Task.Run(async () => { await Task.Delay(1000, cancellationToken) }, cancellationToken)) + // .Callback((roles, databaseName) => + // { + // cts.Cancel(); + // }); + var logger = new Mock>(); + + // Act + var controller = new CategoriesController(mediator.Object, logger.Object); + const long id = 1; + var result = await controller.GetByIdAsync(id, cancellationToken); + + // Asser + logger.VerifyLog(_ => _.LogError(It.IsAny())); + logger.VerifyLog(_ => _.LogError(It.IsNotNull())); + logger.VerifyLog(_ => _.LogError(It.Is(msg => msg.Length > 0))); + logger.VerifyLog(_ => _.LogError("GetByIdAsync: not found id = {Id}", id)); + } + + #endregion + + #region Tests for Func Methods + + [Fact] + public async Task CategoryControllerTests_GetById_Test() + { + //Arrange + var mediator = new Mock(); + + CancellationTokenSource cts = new(); + + var cancellationToken = cts.Token; + + var expectedResult = new GetCategoryQueryResult( + id: 1, + modifiedDate: 1, + addedDate: 1, + deletedDate: 1, + isDeleted: false, + customerId: "1", + categoryName: "categoryName", + categoryDescription: "categoryDescription", + quantityCertificates: 1, + quantityPublishCode: 1, + categorySeries: "categorySeries" + ); + + mediator.Setup(_ => _.Send(It.IsAny(), cancellationToken)) + .Returns(() => Task.FromResult( + expectedResult + )); + + var logger = new Mock>(); + + // Act + var controller = new CategoriesController(mediator.Object, logger.Object); + const long id = 1; + var actionResult = await controller.GetByIdAsync(id, cancellationToken); + var okObjectResult = actionResult as OkObjectResult; + + // Assert + Assert.NotNull(okObjectResult); + Assert.Equal(expectedResult,okObjectResult?.Value); + } + + + #endregion +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Features/Certificates/CertificatesControllerTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Features/Certificates/CertificatesControllerTests.cs new file mode 100644 index 00000000..878c2eac --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Features/Certificates/CertificatesControllerTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using O2NextGen.CertificateManagement.Application.Features.Certificates; +using Tests.O2NextGen.CertificateManagement.Application.Base; +using Xunit; + +namespace Tests.O2NextGen.CertificateManagement.Application.Features.Certificates; + +public class CertificatesControllerTests: BaseControllerTests +{ + [Theory] + [InlineData("CertificatesController")] + public override void It_CheckClassName(string name = "") + { + base.It_CheckClassName(name); + } + + [Fact] + public void ControllerBaseTests_AttributeApiVersion_Supported_v1_0() + { + Assert.Contains(Attribute.GetCustomAttributes(typeof(CertificatesController), typeof(ApiVersionAttribute)), att => + ((ApiVersionAttribute) att).Versions.Any(x => x is {MajorVersion: 1, MinorVersion: 0})); + } + + [Fact] + public void ControllerBaseTests_AttributeApiVersion_Supported_v1_1() + { + Assert.Contains(Attribute.GetCustomAttributes(typeof(CertificatesController), typeof(ApiVersionAttribute)), att => + ((ApiVersionAttribute) att).Versions.Any(x => x is {MajorVersion: 1, MinorVersion: 1})); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/ServiceUrlsTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/ServiceUrlsTests.cs new file mode 100644 index 00000000..f717ac68 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/ServiceUrlsTests.cs @@ -0,0 +1,27 @@ +using O2NextGen.CertificateManagement.Application; +using Tests.O2NextGen.CertificateManagement.Application.Base; +using Xunit; + +namespace Tests.O2NextGen.CertificateManagement.Application; + +public class ServiceUrlsTests : BaseConfigTests +{ + [Theory] + [InlineData("ServiceUrls")] + public override void It_CheckClassName(string name = "") + { + base.It_CheckClassName(name); + } + + [Fact] + public void UrlsConfigTests_PropertyIdentityUrl_IsType() + { + Assert.True(BaseHelper.It_CheckExistPropertyOfType("IdentityUrl", typeof(string))); + } + + [Fact] + public void UrlsConfigTests_PropertyCGenUrl_IsType() + { + Assert.True(BaseHelper.It_CheckExistPropertyOfType("CGenUrl", typeof(string))); + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Tests.O2NextGen.CertificateManagement.Application.csproj b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Tests.O2NextGen.CertificateManagement.Application.csproj new file mode 100644 index 00000000..320e239b --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Application/Tests.O2NextGen.CertificateManagement.Application.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Domain/Tests.O2NextGen.CertificateManagement.Domain.csproj b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Domain/Tests.O2NextGen.CertificateManagement.Domain.csproj new file mode 100644 index 00000000..35cd8dcc --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Domain/Tests.O2NextGen.CertificateManagement.Domain.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Domain/UnitTest1.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Domain/UnitTest1.cs new file mode 100644 index 00000000..d8bd405b --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Domain/UnitTest1.cs @@ -0,0 +1,11 @@ +using Xunit; + +namespace Tests.O2NextGen.CertificateManagement.Domain; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + } +} \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.csproj b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.csproj index 263a4732..e899dc0c 100644 --- a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.csproj +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2 + net6.0 false diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.v3.ncrunchproject b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Impl/Tests.O2NextGen.CertificateManagement.Impl.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Data/Queries/QueryHandlerTests.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Data/Queries/QueryHandlerTests.cs new file mode 100644 index 00000000..37497c6b --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Data/Queries/QueryHandlerTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using O2NextGen.CertificateManagement.Domain.Data; +using O2NextGen.CertificateManagement.Infrastructure.Data; + +namespace Tests.O2NextGen.CertificateManagement.Infrastructure.Data.Queries +{ + public class QueryHandlerTests + { + private static readonly Type QueryType = typeof(IQuery<>); + private static readonly Type QueryHandlerType = typeof(IQueryHandler<,>); + + [Fact] + public void AllDomainQueriesHaveMatchingInfrastructureHandlerTest() + { + var existingQueries = GetAllQueries(); + var existingQueryHandlers = GetAllQueryHandlers(); + + Assert.All( + existingQueries, + query => existingQueryHandlers.Single(handler => handler.GenericTypeArguments[0] == query)); + } + + private static IEnumerable GetAllQueries() + { + return QueryType.Assembly + .GetTypes() + .Where(t => t.IsClass && t.GetInterfaces().SingleOrDefault(IsQueryInterface) != null); + + static bool IsQueryInterface(Type @interface) + => @interface.IsGenericType && @interface.GetGenericTypeDefinition() == QueryType; + } + + private static IEnumerable GetAllQueryHandlers() + { + var infrastructureAssembly = typeof(CGenDbContext).Assembly; + + return infrastructureAssembly + .GetTypes() + .Where(t => t.IsClass && t.GetInterfaces().SingleOrDefault(IsQueryHandlerInterface) != null) + .Select(t => t.GetInterfaces().Single(IsQueryHandlerInterface)); + + static bool IsQueryHandlerInterface(Type @interface) + => @interface.IsGenericType && @interface.GetGenericTypeDefinition() == QueryHandlerType; + } + } +} diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Tests.O2NextGen.CertificateManagement.Infrastructure.csproj b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Tests.O2NextGen.CertificateManagement.Infrastructure.csproj new file mode 100644 index 00000000..14244ff1 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Tests.O2NextGen.CertificateManagement.Infrastructure.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Tests.O2NextGen.CertificateManagement.Infrastructure.v3.ncrunchproject b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Tests.O2NextGen.CertificateManagement.Infrastructure.v3.ncrunchproject new file mode 100644 index 00000000..5d07ca51 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Tests.O2NextGen.CertificateManagement.Infrastructure.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Usings.cs b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/src/Services/c-gen/Tests/Tests.O2NextGen.CertificateManagement.Infrastructure/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Services/c-gen/docker-compose.dcproj b/src/Services/c-gen/docker-compose.dcproj new file mode 100644 index 00000000..d96c156a --- /dev/null +++ b/src/Services/c-gen/docker-compose.dcproj @@ -0,0 +1,14 @@ + + + + 2.1 + Linux + {24414F70-A58D-4802-A437-B15D9F98AC8E} + True + {Scheme}://localhost:{ServicePort} + o2nextgen.certificatemanagement.api + + + + + diff --git a/src/Services/c-gen/docker-compose.override.yml b/src/Services/c-gen/docker-compose.override.yml new file mode 100644 index 00000000..1f18e6df --- /dev/null +++ b/src/Services/c-gen/docker-compose.override.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + o2nextgen.certificatemanagement.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + ports: + - "80" diff --git a/src/Services/c-gen/docker-compose.yml b/src/Services/c-gen/docker-compose.yml new file mode 100644 index 00000000..38500f3f --- /dev/null +++ b/src/Services/c-gen/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + o2nextgen.certificatemanagement.api: + image: ${DOCKER_REGISTRY-}o2nextgencertificatemanagement + build: + context: . + dockerfile: Dockerfile diff --git a/src/Services/c-gen/global.json b/src/Services/c-gen/global.json new file mode 100644 index 00000000..f443bd42 --- /dev/null +++ b/src/Services/c-gen/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/src/Services/e-sender/O2NextGen.ESender.Api/Properties/launchSettings.json b/src/Services/e-sender/O2NextGen.ESender.Api/Properties/launchSettings.json index 822f9fd5..d655c9cf 100644 --- a/src/Services/e-sender/O2NextGen.ESender.Api/Properties/launchSettings.json +++ b/src/Services/e-sender/O2NextGen.ESender.Api/Properties/launchSettings.json @@ -21,17 +21,16 @@ "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", + "applicationUrl": "https://localhost:10004;http://localhost:5004", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:10004;http://localhost:5004" + } }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/values", - "publishAllPorts": true, - "useSSL": true + "environmentVariables": {} } } } \ No newline at end of file diff --git a/src/Services/media-basket/O2NextGen.MediaBasket.Api/Properties/launchSettings.json b/src/Services/media-basket/O2NextGen.MediaBasket.Api/Properties/launchSettings.json index a7ef2502..b02b8e8d 100644 --- a/src/Services/media-basket/O2NextGen.MediaBasket.Api/Properties/launchSettings.json +++ b/src/Services/media-basket/O2NextGen.MediaBasket.Api/Properties/launchSettings.json @@ -18,17 +18,16 @@ "O2NextGen.MediaBasket.Api": { "commandName": "Project", "launchBrowser": true, + "applicationUrl": "https://localhost:46897;http://localhost:22944", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:46897;http://localhost:22944" + } }, "Docker (1)": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", - "publishAllPorts": true, - "useSSL": true + "environmentVariables": {} } } } \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/.dockerignore b/src/Services/o2bionics-id/src/.dockerignore new file mode 100644 index 00000000..bdca33b4 --- /dev/null +++ b/src/Services/o2bionics-id/src/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/Dockerfile b/src/Services/o2bionics-id/src/Dockerfile new file mode 100644 index 00000000..0cad3dde --- /dev/null +++ b/src/Services/o2bionics-id/src/Dockerfile @@ -0,0 +1,21 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["O2Bionics.Services.IdServer/O2Bionics.Services.IdServer.csproj", "O2Bionics.Services.IdServer/"] +RUN dotnet restore "O2Bionics.Services.IdServer/O2Bionics.Services.IdServer.csproj" +COPY . . +WORKDIR "/src/O2Bionics.Services.IdServer" +RUN dotnet build "O2Bionics.Services.IdServer.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "O2Bionics.Services.IdServer.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "O2Bionics.Services.IdServer.dll"] diff --git a/src/Services/o2bionics-id/src/O2Bionics.IdServer.sln b/src/Services/o2bionics-id/src/O2Bionics.IdServer.sln new file mode 100644 index 00000000..b92d0eb3 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.IdServer.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "O2Bionics.Services.IdServer", "O2Bionics.Services.IdServer\O2Bionics.Services.IdServer.csproj", "{4563EFED-8AB0-4E10-B0F2-021D70E863C4}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{5B31C4B1-B62E-48E3-8114-5D5900CC5D43}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4563EFED-8AB0-4E10-B0F2-021D70E863C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4563EFED-8AB0-4E10-B0F2-021D70E863C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4563EFED-8AB0-4E10-B0F2-021D70E863C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4563EFED-8AB0-4E10-B0F2-021D70E863C4}.Release|Any CPU.Build.0 = Release|Any CPU + {5B31C4B1-B62E-48E3-8114-5D5900CC5D43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B31C4B1-B62E-48E3-8114-5D5900CC5D43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B31C4B1-B62E-48E3-8114-5D5900CC5D43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B31C4B1-B62E-48E3-8114-5D5900CC5D43}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {446939C9-ABB5-45A0-A4E9-684A37B2D2AC} + EndGlobalSection +EndGlobal diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Certificates/GenerateCertificateOptions.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Certificates/GenerateCertificateOptions.cs new file mode 100644 index 00000000..e109f065 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Certificates/GenerateCertificateOptions.cs @@ -0,0 +1,65 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace O2Bionics.Services.IdServer.Certificates; + +public class certificateHelper +{ + public static void MakeCert(GenerateCertificateOptions options) + { + var rsa = RSA.Create(4096); + var req = new CertificateRequest($"cn={options.CommonName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(options.Years)); + var path = Path.Combine(options.PathToSave, options.CertificateFileName); + + // Create PFX (PKCS #12) with private key + File.WriteAllBytes($"{path}.pfx", cert.Export(X509ContentType.Pfx, options.Password)); + + // Create Base 64 encoded CER (public key only) + File.WriteAllText($"{path}.cer", + "-----BEGIN CERTIFICATE-----\r\n" + + Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks) + + "\r\n-----END CERTIFICATE-----"); + } +} +public class GenerateCertificateOptions +{ + public GenerateCertificateOptions(string pathToSave, string commonName, string fileName, string password, int years = 1) + { + CommonName = commonName; + if (string.IsNullOrEmpty(CommonName)) + { + throw new ArgumentNullException(nameof(CommonName)); + } + + PathToSave = pathToSave; + if (string.IsNullOrEmpty(PathToSave)) + { + throw new ArgumentNullException(nameof(PathToSave)); + } + + Password = password; + if (string.IsNullOrEmpty(Password)) + { + throw new ArgumentNullException(nameof(Password)); + } + + CertificateFileName = fileName; + if (string.IsNullOrEmpty(CertificateFileName)) + { + throw new ArgumentNullException(nameof(CertificateFileName)); + } + + Years = years; + if (Years <= 0) + { + Years = 1; + } + } + + public string CommonName { get; } + public string PathToSave { get; } + public string Password { get; } + public string CertificateFileName { get; } + public int Years { get; } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DataProtectionSettings.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DataProtectionSettings.cs new file mode 100644 index 00000000..c95bf25f --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DataProtectionSettings.cs @@ -0,0 +1,11 @@ +namespace O2Bionics.Services.IdServer; + +public class DataProtectionSettings +{ + public string KeyVaultKeyId { get; set; } + public string AadTenantId { get; set; } + public string StorageAccountName { get; set; } + public string StorageKeyContainerName { get; set; } + public string StorageKeyBlobName { get; set; } + public string StorageDevKeyBlobName { get; set; } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DbContexts/ApplicationDbContext.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DbContexts/ApplicationDbContext.cs new file mode 100644 index 00000000..10c2fba7 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DbContexts/ApplicationDbContext.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using O2Bionics.Services.IdServer.Models; + +namespace O2Bionics.Services.IdServer.DbContexts +{ + public class ApplicationDbContext: IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options):base(options) + { + + } + + public DbSet DataProtectionKeys { get; } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DbContexts/DataProtectedDbContext.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DbContexts/DataProtectedDbContext.cs new file mode 100644 index 00000000..38b8bbc3 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/DbContexts/DataProtectedDbContext.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace O2Bionics.Services.IdServer.DbContexts; + +public class DataProtectedDbContext: DbContext, IDataProtectionKeyContext +{ + public DbSet DataProtectionKeys { get; } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Dockerfile b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Dockerfile new file mode 100644 index 00000000..0cad3dde --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Dockerfile @@ -0,0 +1,21 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["O2Bionics.Services.IdServer/O2Bionics.Services.IdServer.csproj", "O2Bionics.Services.IdServer/"] +RUN dotnet restore "O2Bionics.Services.IdServer/O2Bionics.Services.IdServer.csproj" +COPY . . +WORKDIR "/src/O2Bionics.Services.IdServer" +RUN dotnet build "O2Bionics.Services.IdServer.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "O2Bionics.Services.IdServer.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "O2Bionics.Services.IdServer.dll"] diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/IdentityServer4_certificate.cer b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/IdentityServer4_certificate.cer new file mode 100644 index 00000000..67dac76e --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/IdentityServer4_certificate.cer @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEtTCCAp2gAwIBAgIJAJO+0VqK2f6QMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMTD0lkZW50 +aXR5U2VydmVyNDAeFw0yMzAxMzExMTUwMDVaFw0yODAxMzExMTUwMDVaMBoxGDAWBgNVBAMTD0lk +ZW50aXR5U2VydmVyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM38IAeC+DbwYIJT +NEoII0B7dOy0SfumO9lPs+E9Gqj7ODR69tRRQ44GWQumOXP7MCdkaR5V6vfOU3zwM+gQtnWcQ2j1 +WxmNmYmrrWe9DdzJndj9qsBGZJO8Ev7bT9xbcqKglv1o89+0aBxE7Yl8M2s1cigsoNZMyJ5YZCxH +9EwedUHZfqSTcfBWYIZwcZKLv1cQsGOylIEcEEnMfB0ccM22QX7RmWSRM31heo9LXQ0dgJCX2VSh +4ekVIomrEc4bNgvGE1thNGUds5qOWiqMtEoAtZMEMr4/AAp+pz4JPXgAc9cC6pEd2Yl7h5PqXPv1 +iL7mkZOzdve+7mHQ25Uo78iP4h54cpaoKrwUQWbL0uU6QnIvuNfiSJyJO6GpyKgalJsStkO7e/GC +FH0/j4VYq+FnZjJzYSKUM3yHzWEMdVDug67LIewTKK0RWj62Na9zbN/LdEfNBoWaMk5YT/Fuaulj +pEXs4xc99I/XLQ2iZCuiizTTSHL9/WqdGQP7RrK6LkJAqybpIDXQ9g5rWlneoFkRTUJOvMbbTS+v +O//au3kLDDnxgnCwVVYHnCTumaWahOtLeA6sQMCgDDKeplggEQWwF8NeY+X4rKGO797EfToJVeaq +fRZ45W0KdsYW5G7A/8G7lj/OdI+G6baJAFdqd6qGUUXF+7piaBZymAwbdnSNAgMBAAEwDQYJKoZI +hvcNAQELBQADggIBAJibBxjSUTNwL85eRXhLBMD5Dj9OOS5tIlPYZjamV5C7Wqs1gp2lMjogLw7r +1EUhADuulyCAWaCryKEubYH1a9MrPhpbEw7vswb329JNQb7uxvypHtfXh4FaccP/4fRVzVGCiQDp +rYsoxfMrVsrCqV70PATdmlpvB+OyvcGT3LKA5xIKd5sk1XyE0RzBoBeWW3/hIeZ9mE8y7a8YoLAx +WYNEIv4YHd9iCsDnTK/tjCQvM0BKHI0ERkRfolvNT51FEnmvcBqwvBymyutxe5UpVGqB0HSSQh2a +ukZb40rDCkxZNs+BzPfICTEqyVP5iOu1HKAZ81JRfv0iOii9bZOWmGWtAiCCYILjgGjstoNYyrtL +HQJy4U3LZmF3gCtQsvaIRgTCJpWcOrhhpvR++SptHU/VAUunVG9fcBW9qUtjNvQmZ9QT/lb5GyK9 +xG1+gvwH9iul+9ldFvPcZgP/XfAaL83mpFVU0zps2shFTzXlvAUshDNXhZjGUo03MQmfYPKAXwLC +yEg0Rz4szemFAosLuBqX9Zv4hhOCOYTrMK7G5ckZShrHk+NaarLEOBprBhS2hM1JH2zj/diJeOxx +0baX5UBklOHo3xif0lRG7WJIO2G8jm32GPgyJNHe3iPfe2ZVhJ9RJ8cHXzrIqyeZzRbQs8KmGYnp +xa0vpZviFfh+uzAs +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/IdentityServer4_certificate.pfx b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/IdentityServer4_certificate.pfx new file mode 100644 index 00000000..ed2457db Binary files /dev/null and b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/IdentityServer4_certificate.pfx differ diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Initializer/DbInitializer.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Initializer/DbInitializer.cs new file mode 100644 index 00000000..07881c63 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Initializer/DbInitializer.cs @@ -0,0 +1,85 @@ +using System.Security.Claims; +using IdentityModel; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using O2Bionics.Services.IdServer.DbContexts; +using O2Bionics.Services.IdServer.Models; + +namespace O2Bionics.Services.IdServer.Initializer; + +public class DbInitializer : IDbInitializer +{ + private readonly UserManager _manager; + private readonly ApplicationDbContext _db; + private readonly RoleManager _roleManager; + + public DbInitializer(UserManager manager, ApplicationDbContext db, + RoleManager roleManager) + { + _manager = manager; + _db = db; + _roleManager = roleManager; + } + + public void Initialize() + { + _db.Database.Migrate(); + if (_roleManager.FindByNameAsync(SD.Admin).Result == null) + { + _roleManager.CreateAsync(new IdentityRole(SD.Admin)).GetAwaiter().GetResult(); + _roleManager.CreateAsync(new IdentityRole(SD.Customer)).GetAwaiter().GetResult(); + } + else + { + return; + } + + string password = "P@ssword1"; + ApplicationUser adminUser = new ApplicationUser() + { + Id = Guid.NewGuid().ToString(), + UserName = "demo@demo.com", + Email = "demo@demo.com", + EmailConfirmed = true, + PhoneNumber = "1111111111111", + FirstName = "Admin", + LastName = "Root" + }; + + _manager.CreateAsync(adminUser, password).GetAwaiter().GetResult(); + _manager.AddToRoleAsync(adminUser, SD.Admin).GetAwaiter().GetResult(); + + var temp1 = _manager.AddClaimsAsync(adminUser, new Claim [] + { + new Claim(JwtClaimTypes.Name, adminUser.FirstName + " " + adminUser.LastName), + new Claim(JwtClaimTypes.GivenName, adminUser.FirstName ), + new Claim(JwtClaimTypes.FamilyName , adminUser.LastName ), + new Claim(JwtClaimTypes.Role , SD.Admin ) + }).GetAwaiter().GetResult() ; + + + ApplicationUser customerUser = new ApplicationUser() + { + Id = Guid.NewGuid().ToString(), + UserName = "customer@demo.com", + Email = "customer@demo.com", + EmailConfirmed = true, + PhoneNumber = "1111111111111", + FirstName = "Denis", + LastName = "Prox", + }; + + _manager.CreateAsync(customerUser, password).GetAwaiter().GetResult(); + _manager.AddToRoleAsync(customerUser, SD.Customer).GetAwaiter().GetResult(); + + var temp2 = _manager.AddClaimsAsync(customerUser, new Claim[] + { + new Claim(JwtClaimTypes.Name, customerUser.FirstName + " " + customerUser.LastName), + new Claim(JwtClaimTypes.GivenName, customerUser.FirstName ), + new Claim(JwtClaimTypes.FamilyName , customerUser.LastName ), + new Claim(JwtClaimTypes.Role , SD.Customer ) + }).GetAwaiter().GetResult(); + + + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Initializer/IDbInitializer.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Initializer/IDbInitializer.cs new file mode 100644 index 00000000..6cf14fb3 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Initializer/IDbInitializer.cs @@ -0,0 +1,7 @@ +namespace O2Bionics.Services.IdServer.Initializer +{ + public interface IDbInitializer + { + public void Initialize(); + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/AccountController.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/AccountController.cs new file mode 100644 index 00000000..bfa74064 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/AccountController.cs @@ -0,0 +1,571 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using IdentityModel; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Duende.IdentityServer; +using Duende.IdentityServer.Events; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Stores; +using Duende.IdentityServer.Test; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Rendering; +using O2Bionics.Services.IdServer.Models; + +namespace IdentityServerHost.Quickstart.UI +{ + /// + /// This sample controller implements a typical login/logout/provision workflow for local and external accounts. + /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production! + /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval + /// + [SecurityHeaders] + [AllowAnonymous] + public class AccountController : Controller + { + private readonly TestUserStore _users; + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clientStore; + private readonly IAuthenticationSchemeProvider _schemeProvider; + private readonly IEventService _events; + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly RoleManager _roleManager; + + public AccountController( + IIdentityServerInteractionService interaction, + IClientStore clientStore, + IAuthenticationSchemeProvider schemeProvider, + IEventService events, + UserManager userManager, + SignInManager signInManager, + RoleManager roleManager, + TestUserStore users = null) + { + // if the TestUserStore is not in DI, then we'll just use the global users collection + // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) + _users = users ?? new TestUserStore(TestUsers.Users); + + _interaction = interaction; + _clientStore = clientStore; + _schemeProvider = schemeProvider; + _events = events; + _userManager = userManager; + _signInManager = signInManager; + _roleManager = roleManager; + } + + /// + /// Entry point into the login workflow + /// + [HttpGet] + public async Task Login(string returnUrl) + { + // build a model so we know what to show on the login page + var vm = await BuildLoginViewModelAsync(returnUrl); + + if (vm.IsExternalLoginOnly) + { + // we only have one option for logging in and it's an external provider + return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl }); + } + + return View(vm); + } + + /// + /// Handle postback from username/password login + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Login(LoginInputModel model, string button) + { + // check if we are in the context of an authorization request + var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + + // the user clicked the "cancel" button + if (button != "login") + { + if (context != null) + { + // if the user cancels, send a result back into IdentityServer as if they + // denied the consent (even if this client does not require consent). + // this will send back an access denied OIDC error response to the client. + await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); + + // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + if (context.IsNativeClient()) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", model.ReturnUrl); + } + + return Redirect(model.ReturnUrl); + } + else + { + // since we don't have a valid context, then we just go back to the home page + return Redirect("~/"); + } + } + + if (ModelState.IsValid) + { + var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, + model.RememberLogin, lockoutOnFailure: false); + if (result.Succeeded) + { + var user = await _userManager.FindByNameAsync(model.Username); + await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, + user.UserName, clientId: context?.Client.ClientId)); + + if (context != null) + { + return Redirect(model.ReturnUrl); + } + + if (Url.IsLocalUrl(model.ReturnUrl)) + { + return Redirect(model.ReturnUrl); + } + else if (string.IsNullOrEmpty(model.ReturnUrl)) + { + return Redirect("~/"); + } + else + { + throw new Exception("invalid return url"); + } + } + + //for test users + //// validate username/password against in-memory store + //if (_users.ValidateCredentials(model.Username, model.Password)) + //{ + // var user = _users.FindByUsername(model.Username); + // await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, + // clientId: context?.Client.ClientId)); + + // // only set explicit expiration here if user chooses "remember me". + // // otherwise we rely upon expiration configured in cookie middleware. + // AuthenticationProperties props = null; + // if (AccountOptions.AllowRememberLogin && model.RememberLogin) + // { + // props = new AuthenticationProperties + // { + // IsPersistent = true, + // ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) + // }; + // } + + // ; + + // // issue authentication cookie with subject ID and username + // var isuser = new IdentityServerUser(user.SubjectId) + // { + // DisplayName = user.Username + // }; + + // await HttpContext.SignInAsync(isuser, props); + + // if (context != null) + // { + // if (context.IsNativeClient()) + // { + // // The client is native, so this change in how to + // // return the response is for better UX for the end user. + // return this.LoadingPage("Redirect", model.ReturnUrl); + // } + + // // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + // return Redirect(model.ReturnUrl); + // } + + // // request for a local page + // if (Url.IsLocalUrl(model.ReturnUrl)) + // { + // return Redirect(model.ReturnUrl); + // } + // else if (string.IsNullOrEmpty(model.ReturnUrl)) + // { + // return Redirect("~/"); + // } + // else + // { + // // user might have clicked on a malicious link - should be logged + // throw new Exception("invalid return URL"); + // } + //} + + await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", + clientId: context?.Client.ClientId)); + ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage); + } + + // something went wrong, show form with error + var vm = await BuildLoginViewModelAsync(model); + return View(vm); + } + + + /// + /// Show logout page + /// + [HttpGet] + public async Task Logout(string logoutId) + { + // build a model so the logout page knows what to display + var vm = await BuildLogoutViewModelAsync(logoutId); + + if (vm.ShowLogoutPrompt == false) + { + // if the request for logout was properly authenticated from IdentityServer, then + // we don't need to show the prompt and can just log the user out directly. + return await Logout(vm); + } + + return View(vm); + } + + /// + /// Handle logout page postback + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Logout(LogoutInputModel model) + { + // build a model so the logged out page knows what to display + var vm = await BuildLoggedOutViewModelAsync(model.LogoutId); + + if (User?.Identity.IsAuthenticated == true) + { + // delete local authentication cookie + //await HttpContext.SignOutAsync(); + await _signInManager.SignOutAsync(); + // raise the logout event + await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); + } + + // check if we need to trigger sign-out at an upstream identity provider + if (vm.TriggerExternalSignout) + { + // build a return URL so the upstream provider will redirect back + // to us after the user has logged out. this allows us to then + // complete our single sign-out processing. + string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); + + // this triggers a redirect to the external provider for sign-out + return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); + } + + return View("LoggedOut", vm); + } + + [HttpGet] + public IActionResult AccessDenied() + { + return View(); + } + [HttpGet] + public async Task Register(string returnUrl) + { + // build a model so we know what to show on the reg page + var vm = await BuildRegisterViewModelAsync(returnUrl); + + return View(vm); + } + + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model, string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + if (ModelState.IsValid) + { + + var user = new ApplicationUser + { + UserName = model.Username, + Email = model.Email, + EmailConfirmed = true, + FirstName = model.FirstName, + LastName = model.LastName + }; + + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + if (!_roleManager.RoleExistsAsync(model.RoleName).GetAwaiter().GetResult()) + { + var userRole = new IdentityRole + { + Name = model.RoleName, + NormalizedName = model.RoleName, + + }; + await _roleManager.CreateAsync(userRole); + } + + await _userManager.AddToRoleAsync(user, model.RoleName); + + await _userManager.AddClaimsAsync(user, new Claim[]{ + new Claim(JwtClaimTypes.Name, model.Username), + new Claim(JwtClaimTypes.Email, model.Email), + new Claim(JwtClaimTypes.FamilyName, model.FirstName), + new Claim(JwtClaimTypes.GivenName, model.LastName), + new Claim(JwtClaimTypes.WebSite, "http://"+model.Username+".com"), + new Claim(JwtClaimTypes.Role,"User") }); + + var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + var loginresult = await _signInManager.PasswordSignInAsync(model.Username, model.Password, false, lockoutOnFailure: true); + if (loginresult.Succeeded) + { + var checkuser = await _userManager.FindByNameAsync(model.Username); + await _events.RaiseAsync(new UserLoginSuccessEvent(checkuser.UserName, checkuser.Id, checkuser.UserName, clientId: context?.Client.ClientId)); + + if (context != null) + { + if (context.IsNativeClient()) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", model.ReturnUrl); + } + + // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null + return Redirect(model.ReturnUrl); + } + + // request for a local page + if (Url.IsLocalUrl(model.ReturnUrl)) + { + return Redirect(model.ReturnUrl); + } + else if (string.IsNullOrEmpty(model.ReturnUrl)) + { + return Redirect("~/"); + } + else + { + // user might have clicked on a malicious link - should be logged + throw new Exception("invalid return URL"); + } + } + + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + private async Task BuildRegisterViewModelAsync(string returnUrl) + { + var context = await _interaction.GetAuthorizationContextAsync(returnUrl); + var roles = new List + { + "Admin", + "Customer" + }; + SelectList DegreeCompleted = new SelectList(roles); + ViewBag.message = DegreeCompleted; + if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null) + { + var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider; + + // this is meant to short circuit the UI and only trigger the one external IdP + var vm = new RegisterViewModel + { + EnableLocalLogin = local, + ReturnUrl = returnUrl, + Username = context?.LoginHint, + }; + + if (!local) + { + vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } }; + } + + return vm; + } + + var schemes = await _schemeProvider.GetAllSchemesAsync(); + + var providers = schemes + .Where(x => x.DisplayName != null) + .Select(x => new ExternalProvider + { + DisplayName = x.DisplayName ?? x.Name, + AuthenticationScheme = x.Name + }).ToList(); + + var allowLocal = true; + if (context?.Client.ClientId != null) + { + var client = await _clientStore.FindEnabledClientByIdAsync(context.Client.ClientId); + if (client != null) + { + allowLocal = client.EnableLocalLogin; + + if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) + { + providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); + } + } + } + + return new RegisterViewModel + { + AllowRememberLogin = AccountOptions.AllowRememberLogin, + EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, + ReturnUrl = returnUrl, + Username = context?.LoginHint, + ExternalProviders = providers.ToArray() + }; + } + + /*****************************************/ + /* helper APIs for the AccountController */ + /*****************************************/ + private async Task BuildLoginViewModelAsync(string returnUrl) + { + var context = await _interaction.GetAuthorizationContextAsync(returnUrl); + if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null) + { + var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider; + + // this is meant to short circuit the UI and only trigger the one external IdP + var vm = new LoginViewModel + { + EnableLocalLogin = local, + ReturnUrl = returnUrl, + Username = context?.LoginHint, + }; + + if (!local) + { + vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } }; + } + + return vm; + } + + var schemes = await _schemeProvider.GetAllSchemesAsync(); + + var providers = schemes + .Where(x => x.DisplayName != null) + .Select(x => new ExternalProvider + { + DisplayName = x.DisplayName ?? x.Name, + AuthenticationScheme = x.Name + }).ToList(); + + var allowLocal = true; + if (context?.Client.ClientId != null) + { + var client = await _clientStore.FindEnabledClientByIdAsync(context.Client.ClientId); + if (client != null) + { + allowLocal = client.EnableLocalLogin; + + if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) + { + providers = providers.Where(provider => + client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); + } + } + } + + return new LoginViewModel + { + AllowRememberLogin = AccountOptions.AllowRememberLogin, + EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, + ReturnUrl = returnUrl, + Username = context?.LoginHint, + ExternalProviders = providers.ToArray() + }; + } + + private async Task BuildLoginViewModelAsync(LoginInputModel model) + { + var vm = await BuildLoginViewModelAsync(model.ReturnUrl); + vm.Username = model.Username; + vm.RememberLogin = model.RememberLogin; + return vm; + } + + private async Task BuildLogoutViewModelAsync(string logoutId) + { + var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt }; + + if (User?.Identity.IsAuthenticated != true) + { + // if the user is not authenticated, then just show logged out page + vm.ShowLogoutPrompt = false; + return vm; + } + + var context = await _interaction.GetLogoutContextAsync(logoutId); + if (context?.ShowSignoutPrompt == false) + { + // it's safe to automatically sign-out + vm.ShowLogoutPrompt = false; + return vm; + } + + // show the logout prompt. this prevents attacks where the user + // is automatically signed out by another malicious web page. + return vm; + } + + private async Task BuildLoggedOutViewModelAsync(string logoutId) + { + // get context information (client name, post logout redirect URI and iframe for federated signout) + var logout = await _interaction.GetLogoutContextAsync(logoutId); + + var vm = new LoggedOutViewModel + { + AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, + PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, + ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName, + SignOutIframeUrl = logout?.SignOutIFrameUrl, + LogoutId = logoutId + }; + + if (User?.Identity.IsAuthenticated == true) + { + var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; + if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider) + { + var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp); + if (providerSupportsSignout) + { + if (vm.LogoutId == null) + { + // if there's no current logout context, we need to create one + // this captures necessary info from the current logged in user + // before we signout and redirect away to the external IdP for signout + vm.LogoutId = await _interaction.CreateLogoutContextAsync(); + } + + vm.ExternalAuthenticationScheme = idp; + } + } + } + + return vm; + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/AccountOptions.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/AccountOptions.cs new file mode 100644 index 00000000..4997d184 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/AccountOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System; + +namespace IdentityServerHost.Quickstart.UI +{ + public class AccountOptions + { + public static bool AllowLocalLogin = true; + public static bool AllowRememberLogin = true; + public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); + + public static bool ShowLogoutPrompt = true; + public static bool AutomaticRedirectAfterSignOut = false; + + public static string InvalidCredentialsErrorMessage = "Invalid username or password"; + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/ExternalController.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/ExternalController.cs new file mode 100644 index 00000000..e6074a7c --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/ExternalController.cs @@ -0,0 +1,200 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using IdentityModel; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Duende.IdentityServer; +using Duende.IdentityServer.Events; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Stores; +using Duende.IdentityServer.Test; + +namespace IdentityServerHost.Quickstart.UI +{ + [SecurityHeaders] + [AllowAnonymous] + public class ExternalController : Controller + { + private readonly TestUserStore _users; + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clientStore; + private readonly ILogger _logger; + private readonly IEventService _events; + + public ExternalController( + IIdentityServerInteractionService interaction, + IClientStore clientStore, + IEventService events, + ILogger logger, + TestUserStore users = null) + { + // if the TestUserStore is not in DI, then we'll just use the global users collection + // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) + _users = users ?? new TestUserStore(TestUsers.Users); + + _interaction = interaction; + _clientStore = clientStore; + _logger = logger; + _events = events; + } + + /// + /// initiate roundtrip to external authentication provider + /// + [HttpGet] + public IActionResult Challenge(string scheme, string returnUrl) + { + if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; + + // validate returnUrl - either it is a valid OIDC URL or back to a local page + if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false) + { + // user might have clicked on a malicious link - should be logged + throw new Exception("invalid return URL"); + } + + // start challenge and roundtrip the return URL and scheme + var props = new AuthenticationProperties + { + RedirectUri = Url.Action(nameof(Callback)), + Items = + { + { "returnUrl", returnUrl }, + { "scheme", scheme }, + } + }; + + return Challenge(props, scheme); + + } + + /// + /// Post processing of external authentication + /// + [HttpGet] + public async Task Callback() + { + // read external identity from the temporary cookie + var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); + if (result?.Succeeded != true) + { + throw new Exception("External authentication error"); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}"); + _logger.LogDebug("External claims: {@claims}", externalClaims); + } + + // lookup our user and external provider info + var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result); + if (user == null) + { + // this might be where you might initiate a custom workflow for user registration + // in this sample we don't show how that would be done, as our sample implementation + // simply auto-provisions new external user + user = AutoProvisionUser(provider, providerUserId, claims); + } + + // this allows us to collect any additional claims or properties + // for the specific protocols used and store them in the local auth cookie. + // this is typically used to store data needed for signout from those protocols. + var additionalLocalClaims = new List(); + var localSignInProps = new AuthenticationProperties(); + ProcessLoginCallback(result, additionalLocalClaims, localSignInProps); + + // issue authentication cookie for user + var isuser = new IdentityServerUser(user.SubjectId) + { + DisplayName = user.Username, + IdentityProvider = provider, + AdditionalClaims = additionalLocalClaims + }; + + await HttpContext.SignInAsync(isuser, localSignInProps); + + // delete temporary cookie used during external authentication + await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); + + // retrieve return URL + var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; + + // check if external login is in the context of an OIDC request + var context = await _interaction.GetAuthorizationContextAsync(returnUrl); + await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId)); + + if (context != null) + { + if (context.IsNativeClient()) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", returnUrl); + } + } + + return Redirect(returnUrl); + } + + private (TestUser user, string provider, string providerUserId, IEnumerable claims) FindUserFromExternalProvider(AuthenticateResult result) + { + var externalUser = result.Principal; + + // try to determine the unique id of the external user (issued by the provider) + // the most common claim type for that are the sub claim and the NameIdentifier + // depending on the external provider, some other claim type might be used + var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ?? + externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? + throw new Exception("Unknown userid"); + + // remove the user id claim so we don't include it as an extra claim if/when we provision the user + var claims = externalUser.Claims.ToList(); + claims.Remove(userIdClaim); + + var provider = result.Properties.Items["scheme"]; + var providerUserId = userIdClaim.Value; + + // find external user + var user = _users.FindByExternalProvider(provider, providerUserId); + + return (user, provider, providerUserId, claims); + } + + private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable claims) + { + var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); + return user; + } + + // if the external login is OIDC-based, there are certain things we need to preserve to make logout work + // this will be different for WS-Fed, SAML2p or other protocols + private void ProcessLoginCallback(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) + { + // if the external system sent a session id claim, copy it over + // so we can use it for single sign-out + var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); + if (sid != null) + { + localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); + } + + // if the external provider issued an id_token, we'll keep it for signout + var idToken = externalResult.Properties.GetTokenValue("id_token"); + if (idToken != null) + { + localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } }); + } + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/ExternalProvider.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/ExternalProvider.cs new file mode 100644 index 00000000..72a64c38 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/ExternalProvider.cs @@ -0,0 +1,12 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class ExternalProvider + { + public string DisplayName { get; set; } + public string AuthenticationScheme { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoggedOutViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoggedOutViewModel.cs new file mode 100644 index 00000000..8b2a7195 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoggedOutViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class LoggedOutViewModel + { + public string PostLogoutRedirectUri { get; set; } + public string ClientName { get; set; } + public string SignOutIframeUrl { get; set; } + + public bool AutomaticRedirectAfterSignOut { get; set; } + + public string LogoutId { get; set; } + public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; + public string ExternalAuthenticationScheme { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoginInputModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoginInputModel.cs new file mode 100644 index 00000000..fecc1ed2 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoginInputModel.cs @@ -0,0 +1,18 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.ComponentModel.DataAnnotations; + +namespace IdentityServerHost.Quickstart.UI +{ + public class LoginInputModel + { + [Required] + public string Username { get; set; } + [Required] + public string Password { get; set; } + public bool RememberLogin { get; set; } + public string ReturnUrl { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoginViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoginViewModel.cs new file mode 100644 index 00000000..aa63aba9 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LoginViewModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IdentityServerHost.Quickstart.UI +{ + public class LoginViewModel : LoginInputModel + { + public bool AllowRememberLogin { get; set; } = true; + public bool EnableLocalLogin { get; set; } = true; + + public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); + public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); + + public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; + public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LogoutInputModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LogoutInputModel.cs new file mode 100644 index 00000000..debc4e6f --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LogoutInputModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class LogoutInputModel + { + public string LogoutId { get; set; } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LogoutViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LogoutViewModel.cs new file mode 100644 index 00000000..29e39a4f --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/LogoutViewModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class LogoutViewModel : LogoutInputModel + { + public bool ShowLogoutPrompt { get; set; } = true; + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/RedirectViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/RedirectViewModel.cs new file mode 100644 index 00000000..7f16b421 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/RedirectViewModel.cs @@ -0,0 +1,12 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + + +namespace IdentityServerHost.Quickstart.UI +{ + public class RedirectViewModel + { + public string RedirectUrl { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/RegisterViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/RegisterViewModel.cs new file mode 100644 index 00000000..8161a31b --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Account/RegisterViewModel.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace IdentityServerHost.Quickstart.UI; + +public class RegisterViewModel +{ + [Required] + public string Username { get; set; } + + [Required] + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + [Required] + public string Password { get; set; } + + public string ReturnUrl { get; set; } + public string RoleName { get; set; } + + public bool AllowRememberLogin { get; set; } = true; + public bool EnableLocalLogin { get; set; } = true; + + public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); + public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); + + // public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; + // public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; + + +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentController.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentController.cs new file mode 100644 index 00000000..0d190dde --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentController.cs @@ -0,0 +1,262 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using System; +using Duende.IdentityServer.Events; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Validation; + +namespace IdentityServerHost.Quickstart.UI +{ + /// + /// This controller processes the consent UI + /// + [SecurityHeaders] + [Authorize] + public class ConsentController : Controller + { + private readonly IIdentityServerInteractionService _interaction; + private readonly IEventService _events; + private readonly ILogger _logger; + + public ConsentController( + IIdentityServerInteractionService interaction, + IEventService events, + ILogger logger) + { + _interaction = interaction; + _events = events; + _logger = logger; + } + + /// + /// Shows the consent screen + /// + /// + /// + [HttpGet] + public async Task Index(string returnUrl) + { + var vm = await BuildViewModelAsync(returnUrl); + if (vm != null) + { + return View("Index", vm); + } + + return View("Error"); + } + + /// + /// Handles the consent screen postback + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Index(ConsentInputModel model) + { + var result = await ProcessConsent(model); + + if (result.IsRedirect) + { + var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + if (context?.IsNativeClient() == true) + { + // The client is native, so this change in how to + // return the response is for better UX for the end user. + return this.LoadingPage("Redirect", result.RedirectUri); + } + + return Redirect(result.RedirectUri); + } + + if (result.HasValidationError) + { + ModelState.AddModelError(string.Empty, result.ValidationError); + } + + if (result.ShowView) + { + return View("Index", result.ViewModel); + } + + return View("Error"); + } + + /*****************************************/ + /* helper APIs for the ConsentController */ + /*****************************************/ + private async Task ProcessConsent(ConsentInputModel model) + { + var result = new ProcessConsentResult(); + + // validate return url is still valid + var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); + if (request == null) return result; + + ConsentResponse grantedConsent = null; + + // user clicked 'no' - send back the standard 'access_denied' response + if (model?.Button == "no") + { + grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + + // emit event + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); + } + // user clicked 'yes' - validate the data + else if (model?.Button == "yes") + { + // if the user consented to some scope, build the response model + if (model.ScopesConsented != null && model.ScopesConsented.Any()) + { + var scopes = model.ScopesConsented; + if (ConsentOptions.EnableOfflineAccess == false) + { + scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess); + } + + grantedConsent = new ConsentResponse + { + RememberConsent = model.RememberConsent, + ScopesValuesConsented = scopes.ToArray(), + Description = model.Description + }; + + // emit event + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + } + else + { + result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; + } + } + else + { + result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; + } + + if (grantedConsent != null) + { + // communicate outcome of consent back to identityserver + await _interaction.GrantConsentAsync(request, grantedConsent); + + // indicate that's it ok to redirect back to authorization endpoint + result.RedirectUri = model.ReturnUrl; + result.Client = request.Client; + } + else + { + // we need to redisplay the consent UI + result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model); + } + + return result; + } + + private async Task BuildViewModelAsync(string returnUrl, ConsentInputModel model = null) + { + var request = await _interaction.GetAuthorizationContextAsync(returnUrl); + if (request != null) + { + return CreateConsentViewModel(model, returnUrl, request); + } + else + { + _logger.LogError("No consent request matching request: {0}", returnUrl); + } + + return null; + } + + private ConsentViewModel CreateConsentViewModel( + ConsentInputModel model, string returnUrl, + AuthorizationRequest request) + { + var vm = new ConsentViewModel + { + RememberConsent = model?.RememberConsent ?? true, + ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), + Description = model?.Description, + + ReturnUrl = returnUrl, + + ClientName = request.Client.ClientName ?? request.Client.ClientId, + ClientUrl = request.Client.ClientUri, + ClientLogoUrl = request.Client.LogoUri, + AllowRememberConsent = request.Client.AllowRememberConsent + }; + + vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); + + var apiScopes = new List(); + foreach(var parsedScope in request.ValidatedResources.ParsedScopes) + { + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); + if (apiScope != null) + { + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); + apiScopes.Add(scopeVm); + } + } + if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) + { + apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)); + } + vm.ApiScopes = apiScopes; + + return vm; + } + + private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) + { + return new ScopeViewModel + { + Value = identity.Name, + DisplayName = identity.DisplayName ?? identity.Name, + Description = identity.Description, + Emphasize = identity.Emphasize, + Required = identity.Required, + Checked = check || identity.Required + }; + } + + public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) + { + var displayName = apiScope.DisplayName ?? apiScope.Name; + if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) + { + displayName += ":" + parsedScopeValue.ParsedParameter; + } + + return new ScopeViewModel + { + Value = parsedScopeValue.RawValue, + DisplayName = displayName, + Description = apiScope.Description, + Emphasize = apiScope.Emphasize, + Required = apiScope.Required, + Checked = check || apiScope.Required + }; + } + + private ScopeViewModel GetOfflineAccessScope(bool check) + { + return new ScopeViewModel + { + Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess, + DisplayName = ConsentOptions.OfflineAccessDisplayName, + Description = ConsentOptions.OfflineAccessDescription, + Emphasize = true, + Checked = check + }; + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentInputModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentInputModel.cs new file mode 100644 index 00000000..10d7f479 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentInputModel.cs @@ -0,0 +1,17 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.Collections.Generic; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ConsentInputModel + { + public string Button { get; set; } + public IEnumerable ScopesConsented { get; set; } + public bool RememberConsent { get; set; } + public string ReturnUrl { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentOptions.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentOptions.cs new file mode 100644 index 00000000..d436d9c9 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class ConsentOptions + { + public static bool EnableOfflineAccess = true; + public static string OfflineAccessDisplayName = "Offline Access"; + public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; + + public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; + public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentViewModel.cs new file mode 100644 index 00000000..cb110983 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ConsentViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.Collections.Generic; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ConsentViewModel : ConsentInputModel + { + public string ClientName { get; set; } + public string ClientUrl { get; set; } + public string ClientLogoUrl { get; set; } + public bool AllowRememberConsent { get; set; } + + public IEnumerable IdentityScopes { get; set; } + public IEnumerable ApiScopes { get; set; } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ProcessConsentResult.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ProcessConsentResult.cs new file mode 100644 index 00000000..da501941 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ProcessConsentResult.cs @@ -0,0 +1,21 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using Duende.IdentityServer.Models; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ProcessConsentResult + { + public bool IsRedirect => RedirectUri != null; + public string RedirectUri { get; set; } + public Client Client { get; set; } + + public bool ShowView => ViewModel != null; + public ConsentViewModel ViewModel { get; set; } + + public bool HasValidationError => ValidationError != null; + public string ValidationError { get; set; } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ScopeViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ScopeViewModel.cs new file mode 100644 index 00000000..ff1d9dc1 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Consent/ScopeViewModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class ScopeViewModel + { + public string Value { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public bool Emphasize { get; set; } + public bool Required { get; set; } + public bool Checked { get; set; } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceAuthorizationInputModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceAuthorizationInputModel.cs new file mode 100644 index 00000000..272442ad --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceAuthorizationInputModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class DeviceAuthorizationInputModel : ConsentInputModel + { + public string UserCode { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceAuthorizationViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceAuthorizationViewModel.cs new file mode 100644 index 00000000..8cf030c9 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceAuthorizationViewModel.cs @@ -0,0 +1,12 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace IdentityServerHost.Quickstart.UI +{ + public class DeviceAuthorizationViewModel : ConsentViewModel + { + public string UserCode { get; set; } + public bool ConfirmUserCode { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceController.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceController.cs new file mode 100644 index 00000000..9e69aee6 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Device/DeviceController.cs @@ -0,0 +1,232 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duende.IdentityServer.Configuration; +using Duende.IdentityServer.Events; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace IdentityServerHost.Quickstart.UI +{ + [Authorize] + [SecurityHeaders] + public class DeviceController : Controller + { + private readonly IDeviceFlowInteractionService _interaction; + private readonly IEventService _events; + private readonly IOptions _options; + private readonly ILogger _logger; + + public DeviceController( + IDeviceFlowInteractionService interaction, + IEventService eventService, + IOptions options, + ILogger logger) + { + _interaction = interaction; + _events = eventService; + _options = options; + _logger = logger; + } + + [HttpGet] + public async Task Index() + { + string userCodeParamName = _options.Value.UserInteraction.DeviceVerificationUserCodeParameter; + string userCode = Request.Query[userCodeParamName]; + if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture"); + + var vm = await BuildViewModelAsync(userCode); + if (vm == null) return View("Error"); + + vm.ConfirmUserCode = true; + return View("UserCodeConfirmation", vm); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task UserCodeCapture(string userCode) + { + var vm = await BuildViewModelAsync(userCode); + if (vm == null) return View("Error"); + + return View("UserCodeConfirmation", vm); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Callback(DeviceAuthorizationInputModel model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + var result = await ProcessConsent(model); + if (result.HasValidationError) return View("Error"); + + return View("Success"); + } + + private async Task ProcessConsent(DeviceAuthorizationInputModel model) + { + var result = new ProcessConsentResult(); + + var request = await _interaction.GetAuthorizationContextAsync(model.UserCode); + if (request == null) return result; + + ConsentResponse grantedConsent = null; + + // user clicked 'no' - send back the standard 'access_denied' response + if (model.Button == "no") + { + grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; + + // emit event + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); + } + // user clicked 'yes' - validate the data + else if (model.Button == "yes") + { + // if the user consented to some scope, build the response model + if (model.ScopesConsented != null && model.ScopesConsented.Any()) + { + var scopes = model.ScopesConsented; + if (ConsentOptions.EnableOfflineAccess == false) + { + scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess); + } + + grantedConsent = new ConsentResponse + { + RememberConsent = model.RememberConsent, + ScopesValuesConsented = scopes.ToArray(), + Description = model.Description + }; + + // emit event + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + } + else + { + result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; + } + } + else + { + result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; + } + + if (grantedConsent != null) + { + // communicate outcome of consent back to identityserver + await _interaction.HandleRequestAsync(model.UserCode, grantedConsent); + + // indicate that's it ok to redirect back to authorization endpoint + result.RedirectUri = model.ReturnUrl; + result.Client = request.Client; + } + else + { + // we need to redisplay the consent UI + result.ViewModel = await BuildViewModelAsync(model.UserCode, model); + } + + return result; + } + + private async Task BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null) + { + var request = await _interaction.GetAuthorizationContextAsync(userCode); + if (request != null) + { + return CreateConsentViewModel(userCode, model, request); + } + + return null; + } + + private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, DeviceFlowAuthorizationRequest request) + { + var vm = new DeviceAuthorizationViewModel + { + UserCode = userCode, + Description = model?.Description, + + RememberConsent = model?.RememberConsent ?? true, + ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), + + ClientName = request.Client.ClientName ?? request.Client.ClientId, + ClientUrl = request.Client.ClientUri, + ClientLogoUrl = request.Client.LogoUri, + AllowRememberConsent = request.Client.AllowRememberConsent + }; + + vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); + + var apiScopes = new List(); + foreach (var parsedScope in request.ValidatedResources.ParsedScopes) + { + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); + if (apiScope != null) + { + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); + apiScopes.Add(scopeVm); + } + } + if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) + { + apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)); + } + vm.ApiScopes = apiScopes; + + return vm; + } + + private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) + { + return new ScopeViewModel + { + Value = identity.Name, + DisplayName = identity.DisplayName ?? identity.Name, + Description = identity.Description, + Emphasize = identity.Emphasize, + Required = identity.Required, + Checked = check || identity.Required + }; + } + + public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) + { + return new ScopeViewModel + { + Value = parsedScopeValue.RawValue, + // todo: use the parsed scope value in the display? + DisplayName = apiScope.DisplayName ?? apiScope.Name, + Description = apiScope.Description, + Emphasize = apiScope.Emphasize, + Required = apiScope.Required, + Checked = check || apiScope.Required + }; + } + private ScopeViewModel GetOfflineAccessScope(bool check) + { + return new ScopeViewModel + { + Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess, + DisplayName = ConsentOptions.OfflineAccessDisplayName, + Description = ConsentOptions.OfflineAccessDescription, + Emphasize = true, + Checked = check + }; + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Diagnostics/DiagnosticsController.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Diagnostics/DiagnosticsController.cs new file mode 100644 index 00000000..07c82e70 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Diagnostics/DiagnosticsController.cs @@ -0,0 +1,29 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace IdentityServerHost.Quickstart.UI +{ + [SecurityHeaders] + [Authorize] + public class DiagnosticsController : Controller + { + public async Task Index() + { + var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() }; + if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString())) + { + return NotFound(); + } + + var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync()); + return View(model); + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Diagnostics/DiagnosticsViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Diagnostics/DiagnosticsViewModel.cs new file mode 100644 index 00000000..4bfd5cb6 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Diagnostics/DiagnosticsViewModel.cs @@ -0,0 +1,32 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using IdentityModel; +using Microsoft.AspNetCore.Authentication; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; + +namespace IdentityServerHost.Quickstart.UI +{ + public class DiagnosticsViewModel + { + public DiagnosticsViewModel(AuthenticateResult result) + { + AuthenticateResult = result; + + if (result.Properties.Items.ContainsKey("client_list")) + { + var encoded = result.Properties.Items["client_list"]; + var bytes = Base64Url.Decode(encoded); + var value = Encoding.UTF8.GetString(bytes); + + Clients = JsonSerializer.Deserialize(value); + } + } + + public AuthenticateResult AuthenticateResult { get; } + public IEnumerable Clients { get; } = new List(); + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Extensions.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Extensions.cs new file mode 100644 index 00000000..8ed5ab91 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Extensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System; +using Duende.IdentityServer.Models; +using Microsoft.AspNetCore.Mvc; + +namespace IdentityServerHost.Quickstart.UI +{ + public static class Extensions + { + /// + /// Checks if the redirect URI is for a native client. + /// + /// + public static bool IsNativeClient(this AuthorizationRequest context) + { + return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) + && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); + } + + public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri) + { + controller.HttpContext.Response.StatusCode = 200; + controller.HttpContext.Response.Headers["Location"] = ""; + + return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri }); + } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Grants/GrantsController.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Grants/GrantsController.cs new file mode 100644 index 00000000..db95dfa1 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Grants/GrantsController.cs @@ -0,0 +1,97 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duende.IdentityServer.Events; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Stores; +using Microsoft.AspNetCore.Authorization; + +namespace IdentityServerHost.Quickstart.UI +{ + /// + /// This sample controller allows a user to revoke grants given to clients + /// + [SecurityHeaders] + [Authorize] + public class GrantsController : Controller + { + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clients; + private readonly IResourceStore _resources; + private readonly IEventService _events; + + public GrantsController(IIdentityServerInteractionService interaction, + IClientStore clients, + IResourceStore resources, + IEventService events) + { + _interaction = interaction; + _clients = clients; + _resources = resources; + _events = events; + } + + /// + /// Show list of grants + /// + [HttpGet] + public async Task Index() + { + return View("Index", await BuildViewModelAsync()); + } + + /// + /// Handle postback to revoke a client + /// + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Revoke(string clientId) + { + await _interaction.RevokeUserConsentAsync(clientId); + await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId)); + + return RedirectToAction("Index"); + } + + private async Task BuildViewModelAsync() + { + var grants = await _interaction.GetAllUserGrantsAsync(); + + var list = new List(); + foreach(var grant in grants) + { + var client = await _clients.FindClientByIdAsync(grant.ClientId); + if (client != null) + { + var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); + + var item = new GrantViewModel() + { + ClientId = client.ClientId, + ClientName = client.ClientName ?? client.ClientId, + ClientLogoUrl = client.LogoUri, + ClientUrl = client.ClientUri, + Description = grant.Description, + Created = grant.CreationTime, + Expires = grant.Expiration, + IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), + ApiGrantNames = resources.ApiScopes.Select(x => x.DisplayName ?? x.Name).ToArray() + }; + + list.Add(item); + } + } + + return new GrantsViewModel + { + Grants = list + }; + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Grants/GrantsViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Grants/GrantsViewModel.cs new file mode 100644 index 00000000..d7b40091 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Grants/GrantsViewModel.cs @@ -0,0 +1,27 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System; +using System.Collections.Generic; + +namespace IdentityServerHost.Quickstart.UI +{ + public class GrantsViewModel + { + public IEnumerable Grants { get; set; } + } + + public class GrantViewModel + { + public string ClientId { get; set; } + public string ClientName { get; set; } + public string ClientUrl { get; set; } + public string ClientLogoUrl { get; set; } + public string Description { get; set; } + public DateTime Created { get; set; } + public DateTime? Expires { get; set; } + public IEnumerable IdentityGrantNames { get; set; } + public IEnumerable ApiGrantNames { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Home/ErrorViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Home/ErrorViewModel.cs new file mode 100644 index 00000000..349bd503 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Home/ErrorViewModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using Duende.IdentityServer.Models; + +namespace IdentityServerHost.Quickstart.UI +{ + public class ErrorViewModel + { + public ErrorViewModel() + { + } + + public ErrorViewModel(string error) + { + Error = new ErrorMessage { Error = error }; + } + + public ErrorMessage Error { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Home/HomeController.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Home/HomeController.cs new file mode 100644 index 00000000..d2d99910 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/Home/HomeController.cs @@ -0,0 +1,65 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Duende.IdentityServer.Services; + +namespace IdentityServerHost.Quickstart.UI +{ + [SecurityHeaders] + [AllowAnonymous] + public class HomeController : Controller + { + private readonly IIdentityServerInteractionService _interaction; + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + public HomeController(IIdentityServerInteractionService interaction, IWebHostEnvironment environment, ILogger logger) + { + _interaction = interaction; + _environment = environment; + _logger = logger; + } + + public IActionResult Index() + { + if (_environment.IsDevelopment()) + { + // only show in development + return View(); + } + + _logger.LogInformation("Homepage is disabled in production. Returning 404."); + return NotFound(); + } + + /// + /// Shows the error page + /// + public async Task Error(string errorId) + { + var vm = new ErrorViewModel(); + + // retrieve error details from identityserver + var message = await _interaction.GetErrorContextAsync(errorId); + if (message != null) + { + vm.Error = message; + + if (!_environment.IsDevelopment()) + { + // only show in development + message.ErrorDescription = null; + } + } + + return View("Error", vm); + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/SecurityHeadersAttribute.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/SecurityHeadersAttribute.cs new file mode 100644 index 00000000..e198d7f0 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/SecurityHeadersAttribute.cs @@ -0,0 +1,56 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace IdentityServerHost.Quickstart.UI +{ + public class SecurityHeadersAttribute : ActionFilterAttribute + { + public override void OnResultExecuting(ResultExecutingContext context) + { + var result = context.Result; + if (result is ViewResult) + { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) + { + context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) + { + context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + var csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; + // also consider adding upgrade-insecure-requests once you have HTTPS in place for production + //csp += "upgrade-insecure-requests;"; + // also an example if you need client images to be displayed from twitter + // csp += "img-src 'self' https://pbs.twimg.com;"; + + // once for standards compliant browsers + if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) + { + context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); + } + // and once again for IE + if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) + { + context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + var referrer_policy = "no-referrer"; + if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) + { + context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); + } + } + } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/TestUsers.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/TestUsers.cs new file mode 100644 index 00000000..00897fde --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/MainModule/TestUsers.cs @@ -0,0 +1,66 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using IdentityModel; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Json; +using Duende.IdentityServer; +using Duende.IdentityServer.Test; + +namespace IdentityServerHost.Quickstart.UI +{ + public class TestUsers + { + public static List Users + { + get + { + var address = new + { + street_address = "One Hacker Way", + locality = "Heidelberg", + postal_code = 69118, + country = "Germany" + }; + + return new List + { + new TestUser + { + SubjectId = "818727", + Username = "alice", + Password = "alice", + Claims = + { + new Claim(JwtClaimTypes.Name, "Alice Smith"), + new Claim(JwtClaimTypes.GivenName, "Alice"), + new Claim(JwtClaimTypes.FamilyName, "Smith"), + new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), + new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), + new Claim(JwtClaimTypes.WebSite, "http://alice.com"), + new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) + } + }, + new TestUser + { + SubjectId = "88421113", + Username = "bob", + Password = "bob", + Claims = + { + new Claim(JwtClaimTypes.Name, "Bob Smith"), + new Claim(JwtClaimTypes.GivenName, "Bob"), + new Claim(JwtClaimTypes.FamilyName, "Smith"), + new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), + new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), + new Claim(JwtClaimTypes.WebSite, "http://bob.com"), + new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) + } + } + }; + } + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130143359_ConfigureDefaultIdentityTables.Designer.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130143359_ConfigureDefaultIdentityTables.Designer.cs new file mode 100644 index 00000000..5f2db782 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130143359_ConfigureDefaultIdentityTables.Designer.cs @@ -0,0 +1,278 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using O2Bionics.Services.IdServer.DbContexts; + +#nullable disable + +namespace O2Bionics.Services.IdServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230130143359_ConfigureDefaultIdentityTables")] + partial class ConfigureDefaultIdentityTables + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("O2Bionics.Services.IdServer.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130143359_ConfigureDefaultIdentityTables.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130143359_ConfigureDefaultIdentityTables.cs new file mode 100644 index 00000000..ddd37d83 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130143359_ConfigureDefaultIdentityTables.cs @@ -0,0 +1,221 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace O2Bionics.Services.IdServer.Migrations +{ + public partial class ConfigureDefaultIdentityTables : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130145448_addLastnameFirstnameToUser.Designer.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130145448_addLastnameFirstnameToUser.Designer.cs new file mode 100644 index 00000000..8e7c0a3f --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130145448_addLastnameFirstnameToUser.Designer.cs @@ -0,0 +1,286 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using O2Bionics.Services.IdServer.DbContexts; + +#nullable disable + +namespace O2Bionics.Services.IdServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230130145448_addLastnameFirstnameToUser")] + partial class addLastnameFirstnameToUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("O2Bionics.Services.IdServer.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130145448_addLastnameFirstnameToUser.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130145448_addLastnameFirstnameToUser.cs new file mode 100644 index 00000000..af000495 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/20230130145448_addLastnameFirstnameToUser.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace O2Bionics.Services.IdServer.Migrations +{ + public partial class addLastnameFirstnameToUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FirstName", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "LastName", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FirstName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LastName", + table: "AspNetUsers"); + } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 00000000..56858cdc --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,284 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using O2Bionics.Services.IdServer.DbContexts; + +#nullable disable + +namespace O2Bionics.Services.IdServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("O2Bionics.Services.IdServer.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("O2Bionics.Services.IdServer.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Models/ApplicationUser.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Models/ApplicationUser.cs new file mode 100644 index 00000000..213f2f2b --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Models/ApplicationUser.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; + +namespace O2Bionics.Services.IdServer.Models +{ + public class ApplicationUser : IdentityUser + { + public string FirstName { get; set; } + public string LastName { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Models/ErrorViewModel.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Models/ErrorViewModel.cs new file mode 100644 index 00000000..4b9d44e7 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Models/ErrorViewModel.cs @@ -0,0 +1,9 @@ +namespace O2Bionics.Services.IdServer.Models +{ + public class ErrorViewModel + { + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/O2Bionics.Services.IdServer.csproj b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/O2Bionics.Services.IdServer.csproj new file mode 100644 index 00000000..b5c64e89 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/O2Bionics.Services.IdServer.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + enable + enable + Linux + ../docker-compose.dcproj + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Always + + + Always + + + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Program.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Program.cs new file mode 100644 index 00000000..e50b3508 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Program.cs @@ -0,0 +1,121 @@ +using System.ComponentModel; +using System.Security.Cryptography.X509Certificates; +using Duende.IdentityServer.Services; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using O2Bionics.Services.IdServer; +using O2Bionics.Services.IdServer.Certificates; +using O2Bionics.Services.IdServer.DbContexts; +using O2Bionics.Services.IdServer.Initializer; +using O2Bionics.Services.IdServer.Models; +using O2Bionics.Services.IdServer.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => +{ + options.UseSqlServer(builder.Configuration["ConnectionString"]); +}); +// Add services to the container. +builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); +// var dataProtectionKeysLocation = +// builder.Configuration.GetSection(nameof(DataProtectionSettings))?.Location; +// if (!string.IsNullOrWhiteSpace(dataProtectionKeysLocation)) +var settings = builder.Configuration.GetSection("DataProtection").Get(); +Console.WriteLine(" ========================= SETTINGS ========================== "); +Console.WriteLine( + $"DataProtection AadTenantId={settings.AadTenantId} keyId={settings.KeyVaultKeyId} account={settings.StorageAccountName} blob={settings.StorageKeyBlobName} blob-dev={settings.StorageDevKeyBlobName}"); +Console.WriteLine(" ================= END SETTINGS ====================\r\n"); + +// var storageAccount = CloudStorageAccount.Parse( +// "DefaultEndpointsProtocol=https;AccountName=o2nextgen001;AccountKey=twBN1I6NPVBTDfaaowm71LznRdfcN9f5uA8MhcW6fYqCOBxl5wtchEinoE7EVHdi0FLDz2sSd4Io+AStJfqN2A==;EndpointSuffix=core.windows.net"); +// Console.WriteLine($"storageAccount={storageAccount}"); +// var client = storageAccount.CreateCloudBlobClient(); +// var container = client.GetContainerReference(settings.StorageKeyContainerName); +// container.CreateIfNotExistsAsync().GetAwaiter().GetResult(); +// var blobName = IsProduction ? settings.StorageKeyBlobName : settings.StorageDevKeyBlobName; + +var keysFolder = Path.Combine(builder.Environment.ContentRootPath, "temp-keys"); +{ + builder.Services + .AddDataProtection() + // This blob must already exist before the application is run + .PersistKeysToAzureBlobStorage( + "DefaultEndpointsProtocol=https;AccountName=o2nextgen001;AccountKey=twBN1I6NPVBTDfaaowm71LznRdfcN9f5uA8MhcW6fYqCOBxl5wtchEinoE7EVHdi0FLDz2sSd4Io+AStJfqN2A==;EndpointSuffix=core.windows.net", + "dataprotection", "keys.xml"); + // Removing this line below for an initial run will ensure the file is created correctly + //.ProtectKeysWithAzureKeyVault(new Uri(""), new DefaultAzureCredential()); + // .PersistKeysToAzureBlobStorage(container, blobName); + // .PersistKeysToDbContext() + // .SetApplicationName("fow-customer-portal") + // .PersistKeysToFileSystem(new DirectoryInfo(keysFolder)) + // .SetDefaultKeyLifetime(TimeSpan.FromDays(30)) + //.ProtectKeysWithDpapi(); + + // .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeysLocation)); + // TODO: encrypt the keys +} +var pass = "P@55w0rd"; +var filename = "IdentityServer4_certificate"; +var filepath = Path.Combine(builder.Environment.ContentRootPath, filename); +// var options = new GenerateCertificateOptions +// ( +// pathToSave: builder.Environment.ContentRootPath, +// commonName: "IdentityServer4", +// fileName: filename, +// password: pass, +// 5 +// ); +// certificateHelper.MakeCert(options); + +var certificate = new X509Certificate2( + Path.Combine(builder.Environment.ContentRootPath, "IdentityServer4_certificate.pfx"), pass); + +builder.Services.AddIdentityServer(options => + { + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + + options.EmitStaticAudienceClaim = true; + }).AddKeyManagement() + .AddInMemoryIdentityResources(SD.IdentityResources) + .AddInMemoryApiScopes(SD.ApiScopes) + .AddInMemoryClients(SD.GetClients(SD.GetUrls(builder.Configuration))) + .AddAspNetIdentity() + //.AddDeveloperSigningCredential() + .AddSigningCredential(certificate); + +//.AddDeveloperSigningCredential(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddControllersWithViews(); + +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.Use((context, next) => { context.Request.Scheme = "https"; return next(); }); +app.UseStaticFiles(); +using var scope = app.Services.CreateScope(); +var initializer = scope.ServiceProvider.GetRequiredService(); +initializer?.Initialize(); +app.UseRouting(); +app.UseIdentityServer(); +app.UseAuthorization(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + +app.Run(); \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Properties/launchSettings.json b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Properties/launchSettings.json new file mode 100644 index 00000000..b47f518c --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "O2Bionics.Services.IdServer": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:5000;http://localhost:5068", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/SD.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/SD.cs new file mode 100644 index 00000000..16c853bb --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/SD.cs @@ -0,0 +1,88 @@ +using Duende.IdentityServer; +using Duende.IdentityServer.Models; + +namespace O2Bionics.Services.IdServer +{ + public static class SD + { + + public static Dictionary GetUrls(IConfiguration configuration) + { + var urls = new Dictionary(); + urls.Add("PfrMvcUrl", + Environment.GetEnvironmentVariable("Urls:PfrMvcUrl") ?? configuration.GetValue("Urls:PfrMvcUrl")); + Console.WriteLine(" ========================= CONFIG IDServer ========================== "); + foreach (var item in urls) + { + Console.WriteLine($"key={item.Key} value={item.Value}"); + } + + Console.WriteLine(" ================= END SETTINGS ====================\r\n"); + return urls; + } + + public const string Admin = "Admin"; + public const string Customer = "Customer"; + + public static IEnumerable IdentityResources => + new List() + { + new IdentityResources.OpenId(), + new IdentityResources.Email(), + new IdentityResources.Profile() + }; + + public static IEnumerable ApiScopes => new List + { + new ApiScope(name: "cgen.api", displayName: "Acces to CGen API") + }; + + public static IEnumerable GetClients(Dictionary clientUrls) + { + return + new List() + { + new Client() + { + ClientId = "client", + ClientSecrets = {new Secret("secret".Sha256())}, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = + { + "cgen.api", + "profile" + } + }, + new Client() + { + ClientId = "mvc", + ClientSecrets = {new Secret("secret".Sha256())}, + AllowedGrantTypes = GrantTypes.Code, + RedirectUris = + { + $"{clientUrls["PfrMvcUrl"]}/signin-oidc", + "https://localhost:5003/signin-oidc" + }, + PostLogoutRedirectUris = + { + $"{clientUrls["PfrMvcUrl"]}/signout-callback-oidc", + "https://localhost:5003/signout-callback-oidc" + }, + AllowedCorsOrigins = + { + $"{clientUrls["PfrMvcUrl"]}", + "https://localhost:5003" + }, + AllowedScopes = new List() + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + "cgen.api", + "profile" + } + }, + }; + } + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Services/ProfileService.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Services/ProfileService.cs new file mode 100644 index 00000000..5219e567 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Services/ProfileService.cs @@ -0,0 +1,64 @@ +using System.Security.Claims; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Services; +using IdentityModel; +using Microsoft.AspNetCore.Identity; +using O2Bionics.Services.IdServer.Models; + +namespace O2Bionics.Services.IdServer.Services; + +public class ProfileService:IProfileService +{ + private readonly IUserClaimsPrincipalFactory _userClaimsPrincipalFactory; + private readonly UserManager _userMgr; + private readonly RoleManager _roleMgr; + + public ProfileService( + UserManager userMgr, + RoleManager roleMgr, + IUserClaimsPrincipalFactory userClaimsPrincipalFactory) + { + _userMgr = userMgr; + _roleMgr = roleMgr; + _userClaimsPrincipalFactory = userClaimsPrincipalFactory; + } + + public async Task GetProfileDataAsync(ProfileDataRequestContext context) + { + string sub = context.Subject.GetSubjectId(); + ApplicationUser user = await _userMgr.FindByIdAsync(sub); + ClaimsPrincipal userClaims = await _userClaimsPrincipalFactory.CreateAsync(user); + + + List claims = userClaims.Claims.ToList(); + claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList(); + claims.Add(new Claim(JwtClaimTypes.FamilyName, user.LastName)); + claims.Add(new Claim(JwtClaimTypes.GivenName, user.FirstName)); + if (_userMgr.SupportsUserRole) + { + IList roles = await _userMgr.GetRolesAsync(user); + foreach(var rolename in roles) + { + claims.Add(new Claim(JwtClaimTypes.Role, rolename)); + if (_roleMgr.SupportsRoleClaims) + { + IdentityRole role = await _roleMgr.FindByNameAsync(rolename); + if (role != null) + { + claims.AddRange(await _roleMgr.GetClaimsAsync(role)); + } + } + } + } + + context.IssuedClaims = claims; + } + + public async Task IsActiveAsync(IsActiveContext context) + { + string sub = context.Subject.GetSubjectId(); + ApplicationUser user = await _userMgr.FindByIdAsync(sub); + context.IsActive = user != null; + } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Urls.cs b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Urls.cs new file mode 100644 index 00000000..d6c1760a --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Urls.cs @@ -0,0 +1,6 @@ +namespace O2Bionics.Services.IdServer; + +public class Urls +{ + public string PfrMvcUrl { get; set; } +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/AccessDenied.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/AccessDenied.cshtml new file mode 100644 index 00000000..0745189e --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/AccessDenied.cshtml @@ -0,0 +1,7 @@ + +
+
+

Access Denied

+

You do not have access to that resource.

+
+
\ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/LoggedOut.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/LoggedOut.cshtml new file mode 100644 index 00000000..dc9fbf7a --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/LoggedOut.cshtml @@ -0,0 +1,34 @@ +@model LoggedOutViewModel + +@{ + // set this so the layout rendering sees an anonymous user + ViewData["signed-out"] = true; +} + +
+

+ Logout + You are now logged out +

+ + @if (Model.PostLogoutRedirectUri != null) + { +
+ Click here to return to the + @Model.ClientName application. +
+ } + + @if (Model.SignOutIframeUrl != null) + { + + } +
+ +@section scripts +{ + @if (Model.AutomaticRedirectAfterSignOut) + { + + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Login.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Login.cshtml new file mode 100644 index 00000000..3a764c93 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Login.cshtml @@ -0,0 +1,91 @@ +@model LoginViewModel + + \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Logout.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Logout.cshtml new file mode 100644 index 00000000..13acaaeb --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Logout.cshtml @@ -0,0 +1,15 @@ +@model LogoutViewModel + +
+
+

Logout

+

Would you like to logout of IdentityServer?

+
+ +
+ +
+ +
+
+
diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Register.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Register.cshtml new file mode 100644 index 00000000..d210f5a2 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Account/Register.cshtml @@ -0,0 +1,58 @@ +@model IdentityServerHost.Quickstart.UI.RegisterViewModel + \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Consent/Index.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Consent/Index.cshtml new file mode 100644 index 00000000..f8aa10d6 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Consent/Index.cshtml @@ -0,0 +1,104 @@ +@model ConsentViewModel + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/Success.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/Success.cshtml new file mode 100644 index 00000000..050dd91c --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/Success.cshtml @@ -0,0 +1,7 @@ + +
+
+

Success

+

You have successfully authorized the device

+
+
diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/UserCodeCapture.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/UserCodeCapture.cshtml new file mode 100644 index 00000000..6d412613 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/UserCodeCapture.cshtml @@ -0,0 +1,23 @@ +@model string + +
+
+

User Code

+

Please enter the code displayed on your device.

+
+ + + +
+
+
+
+ + +
+ + +
+
+
+
diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/UserCodeConfirmation.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/UserCodeConfirmation.cshtml new file mode 100644 index 00000000..e1d3b196 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Device/UserCodeConfirmation.cshtml @@ -0,0 +1,108 @@ +@model DeviceAuthorizationViewModel + +
+
+ @if (Model.ClientLogoUrl != null) + { + + } +

+ @Model.ClientName + is requesting your permission +

+ @if (Model.ConfirmUserCode) + { +

Please confirm that the authorization request quotes the code: @Model.UserCode.

+ } +

Uncheck the permissions you do not wish to grant.

+
+ +
+
+ +
+
+ +
+ +
+
+ @if (Model.IdentityScopes.Any()) + { +
+
+
+ + Personal Information +
+
    + @foreach (var scope in Model.IdentityScopes) + { + + } +
+
+
+ } + + @if (Model.ApiScopes.Any()) + { +
+
+
+ + Application Access +
+
    + @foreach (var scope in Model.ApiScopes) + { + + } +
+
+
+ } + +
+
+
+ + Description +
+
+ +
+
+
+ + @if (Model.AllowRememberConsent) + { +
+
+ + +
+
+ } +
+
+ +
+
+ + +
+
+ @if (Model.ClientUrl != null) + { + + + @Model.ClientName + + } +
+
+
+
diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Diagnostics/Index.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Diagnostics/Index.cshtml new file mode 100644 index 00000000..e939c0d8 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Diagnostics/Index.cshtml @@ -0,0 +1,64 @@ +@model DiagnosticsViewModel + +
+
+

Authentication Cookie

+
+ +
+
+
+
+

Claims

+
+
+
+ @foreach (var claim in Model.AuthenticateResult.Principal.Claims) + { +
@claim.Type
+
@claim.Value
+ } +
+
+
+
+ +
+
+
+

Properties

+
+
+
+ @foreach (var prop in Model.AuthenticateResult.Properties.Items) + { +
@prop.Key
+
@prop.Value
+ } + @if (Model.Clients.Any()) + { +
Clients
+
+ @{ + var clients = Model.Clients.ToArray(); + for(var i = 0; i < clients.Length; i++) + { + @clients[i] + if (i < clients.Length - 1) + { + , + } + } + } +
+ } +
+
+
+
+
+
+ + + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Grants/Index.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Grants/Index.cshtml new file mode 100644 index 00000000..a60f2261 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Grants/Index.cshtml @@ -0,0 +1,87 @@ +@model GrantsViewModel + +
+
+

Client Application Permissions

+

Below is the list of applications you have given permission to and the resources they have access to.

+
+ + @if (Model.Grants.Any() == false) + { +
+
+
+ You have not given access to any applications +
+
+
+ } + else + { + foreach (var grant in Model.Grants) + { +
+
+
+
+ @if (grant.ClientLogoUrl != null) + { + + } + @grant.ClientName +
+ +
+
+ + +
+
+
+
+ +
    + @if (grant.Description != null) + { +
  • + @grant.Description +
  • + } +
  • + @grant.Created.ToString("yyyy-MM-dd") +
  • + @if (grant.Expires.HasValue) + { +
  • + @grant.Expires.Value.ToString("yyyy-MM-dd") +
  • + } + @if (grant.IdentityGrantNames.Any()) + { +
  • + +
      + @foreach (var name in grant.IdentityGrantNames) + { +
    • @name
    • + } +
    +
  • + } + @if (grant.ApiGrantNames.Any()) + { +
  • + +
      + @foreach (var name in grant.ApiGrantNames) + { +
    • @name
    • + } +
    +
  • + } +
+
+ } + } +
\ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Home/Index.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Home/Index.cshtml new file mode 100644 index 00000000..4b7bb729 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Home/Index.cshtml @@ -0,0 +1,32 @@ +@using System.Reflection + +@{ + var version = typeof(Duende.IdentityServer.Hosting.IdentityServerMiddleware).Assembly.GetCustomAttribute()?.InformationalVersion.Split('+').First(); +} + +
+

+ + Welcome to O2Bionics IDServer + @* (version @version) *@ +

+ + +
diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Home/Privacy.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Home/Privacy.cshtml new file mode 100644 index 00000000..2479fb7d --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/Error.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/Error.cshtml new file mode 100644 index 00000000..4c746e77 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/Error.cshtml @@ -0,0 +1,40 @@ +@model ErrorViewModel + +@{ + var error = Model?.Error?.Error; + var errorDescription = Model?.Error?.ErrorDescription; + var request_id = Model?.Error?.RequestId; +} + +
+
+

Error

+
+ +
+
+
+ Sorry, there was an error + + @if (error != null) + { + + + : @error + + + + if (errorDescription != null) + { +
@errorDescription
+ } + } +
+ + @if (request_id != null) + { +
Request Id: @request_id
+ } +
+
+
diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/Redirect.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/Redirect.cshtml new file mode 100644 index 00000000..ecc31c10 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/Redirect.cshtml @@ -0,0 +1,11 @@ +@model RedirectViewModel + +
+
+

You are now being returned to the application

+

Once complete, you may close this tab.

+
+
+ + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Layout.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000..b9857485 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Layout.cshtml @@ -0,0 +1,28 @@ + + + + + + + + O2Bionics IDServer + + + + + + + + + + +
+ @RenderBody() +
+ + + + + @RenderSection("scripts", required: false) + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Layout.cshtml.css b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Layout.cshtml.css new file mode 100644 index 00000000..c04d2dfc --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Layout.cshtml.css @@ -0,0 +1,48 @@ +/* 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; +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Nav.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Nav.cshtml new file mode 100644 index 00000000..dc12e2c3 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_Nav.cshtml @@ -0,0 +1,32 @@ +@using Duende.IdentityServer.Extensions +@{ + string name = null; + if (!true.Equals(ViewData["signed-out"])) + { + name = Context.User?.GetDisplayName(); + } +} + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ScopeListItem.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ScopeListItem.cshtml new file mode 100644 index 00000000..15eebcbd --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ScopeListItem.cshtml @@ -0,0 +1,34 @@ +@model ScopeViewModel + +
  • + + @if (Model.Required) + { + (required) + } + @if (Model.Description != null) + { + + } +
  • \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ValidationScriptsPartial.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 00000000..ff9c7938 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ValidationSummary.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ValidationSummary.cshtml new file mode 100644 index 00000000..674d68d8 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/Shared/_ValidationSummary.cshtml @@ -0,0 +1,7 @@ +@if (ViewContext.ModelState.IsValid == false) +{ +
    + Error +
    +
    +} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/_ViewImports.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/_ViewImports.cshtml new file mode 100644 index 00000000..d0a0bc13 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using IdentityServerHost.Quickstart.UI +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/_ViewStart.cshtml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/appsettings.Development.json b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/appsettings.Development.json new file mode 100644 index 00000000..f1dd9ff7 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "ConnectionString": "Server=52.185.108.164;Initial Catalog=O2Bionics.O2NextGen.IdServerDb-Dev;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30;", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Urls": { + "PfrMvcUrl":"https://localhost:5003" + }, + "DataProtection": { + "KeyVaultKeyId": "https://identity.o2bus.com/keys/DataProtectionKey/bfc1bda979bc4081b89ab6f43bad12b8", + "AadTenantId": "f3a52f65-e3a4-4386-8bc9-a42f32fc1cd6", + "StorageAccountName": "o2nextgen001", + "StorageKeyContainerName": "dataprotection", + "StorageKeyBlobName": "keys.xml", + "StorageDevKeyBlobName": "dev-keys.xml" + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/appsettings.json b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/appsettings.json new file mode 100644 index 00000000..cfcbf398 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/appsettings.json @@ -0,0 +1,21 @@ +{ + "ConnectionString": "Server=52.185.108.164;Initial Catalog=O2Bionics.O2NextGen.IdServerDb;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;Connection Timeout=30;", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Urls": { + "PfrMvcUrl": "https://localhost:5003" + }, + "DataProtection": { + "KeyVaultKeyId": "https://identity.o2bus.com/keys/DataProtectionKey/bfc1bda979bc4081b89ab6f43bad12b8", + "AadTenantId": "f3a52f65-e3a4-4386-8bc9-a42f32fc1cd6", + "StorageAccountName": "o2nextgen001", + "StorageKeyContainerName": "dataprotection", + "StorageKeyBlobName": "keys.xml", + "StorageDevKeyBlobName": "dev-keys.xml" + } +} diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/keys/is-signing-key-AD3BF3D0F45C62DBAFA167E4BF2A24C5.json b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/keys/is-signing-key-AD3BF3D0F45C62DBAFA167E4BF2A24C5.json new file mode 100644 index 00000000..7d44fff3 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/keys/is-signing-key-AD3BF3D0F45C62DBAFA167E4BF2A24C5.json @@ -0,0 +1 @@ +{"Version":1,"Id":"AD3BF3D0F45C62DBAFA167E4BF2A24C5","Created":"2023-01-30T17:41:54.6894786Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8IqoF9pMDRlGrKAizM-dhYdWjzNHx0Gy0lSNci6wxMr6UQIj_u1anmT3Zpdz_7GuxgAft4RDGBQImivMIvrpIG6yri2En9JfxzosPiMX5bbL-_YgKBnjOTAK9kKxa_Eocqx1Imatv0LcQn5Z4UCOZew0FuJswhJx-0gzMvmTEVkB9nD3Mt0CjTsv-aI_dMY661JpVPRJEUnW8_w90NN-tTMEhjrsDhEJoUHcLAD3CrGFDJTMCGM9TgUCcNsGcpwImxfk16reRnZ0nGOTZbXrIYqn5paxq01oqOnwqXgaEl9JvSagIoTnBA67GwbGkFOYZLB_V44GDfKm-6BpcrmY7o0_K150z_OXU1MJz5XRzcZF6BZUzFK4qcH2wNC8IdXx_THsk6qQ3zrxleAmWTGTuSYECC15c1SMIQZANzWNXdiYMitdWIFtF8GouzX0hwfeVHZ9t2iajI1vsc_OpL1tl718Hp_SDOLj7iC4_O0OBFT9pRZiGBWN3IzNZv-eOvpIvwwM_V8AC0xKyI4Q7P7g_Jyeq2I_BmUATxrbJUn3ju6SFKcOZL38Q4c5BqjBEau0vTm4bQGPECgX5_neW_-uSqvR1mvJkAA6mIvsRW5T_Q_qsKdHJLEz-OmX2cOaV0kUBnbmnSSqYFKTAvzMVsIDs4yJKD4jfzrKcDn4ZJqqjuWEgtdjoAqF_EutDvF4CbiAfl21f9OrUily10rkKwJcWtqcpDvkz9J3qADPj6XVUuU2Vy4AjKVjjTW-9qdpD_1pRSiyLSI-9RG6r-AsUQEAEr4ctgKcJhd3q2xl7Y-zGBgg99GxJ1OFJdUqRFfJ3joMmshFsUJWilV1lmh2FIL_iCd59BStBwfLOoKViYPNEjU_d77XPu1KdgNWKFXHOtzMDv6dYE1S0CBB0qNQpK_A4LCHC9IcNH4MFP54gII0X-rftk-e-7cpG7kusg4xyypumMj-m7sMIVb3ej4WzQii0ACt7kBKb-9o6j88dYhpHNbecRMUOutHQd2mr92c5_GLJWXQbHBRPyhpY6rM4iXAFfQD2ciD5wlTYoL0iiBa2pfWz5-HgHHOYJjFcOfAYtCubxoyc-yiM5BB7cFPojcjqt-VFJKzLRA810gHC0BcZ6yqDl02EKxyvPdWgJVODPBLWRb0PmNXBUMfh0dfzVXwHj_eBvnCgySM6pQRCABhe_8zZ8uQ4DRqeIaJy5b4T0UE90YFZ24ouZkP8rQyzAiEs_7BC0JrmtJYcEb-wuXE0CuaBoHUUOrDLTH_lQ8SVnsEMv-oOSDzZ9sj4tmOx-WJ7MJixO-6pEMojrFnAHkcXhGwZZUMI_JGl3FoNFxC-rMkYaYO_Y0-DWLOvZGJjXAnnumGWdFE8eQQtuxAiT0RzOf-FdzLPunCA3uHsVN0lNlVthGgHh2f5PbmXmwES3bp4eWCaCrsEyCvuFUD1Il6HK1kZR7KWKTmKp4kkd-76aDyOUqV5VLd1J0WuvJfOo5_ruDV0vfs612mGwlyxMWH-WfMH6A_B8oE82xAkfWuTtzYXNB62mzwm03iMX2-e3zmUoabmq9EEeA9ziz6TVQxIlwz54H5co6uvMwc3_hAAow0vDuL5Utl2yqkF8KkwA6ihkL0lQyRpI7ilwwYtJBjzrXfEZss1BWTdTB4BvJa_4f8zfO6G9hQIDaLOtpXlKLo8y0513O8G34UVBIlQLS8vrEirU6d1B-AdRJzNYJRw40PomsIlQBL0ZR4s0Vgc7y2TrtJJu2c0hI-qtpuAtW48nP_q2yNmqUePhSLIFeYECIb0woJVRGEn7OFwJN3Ci-qMr7Uy6HPe17DIoKNJBVIjpYzs20UYaUpyPqA9RBXxwx5IcFD48Ko61tB9bFYsEfOsK6-GSj4rynK8h53IdrHTllQv9Vh9n17dhUVSTb_cVt7_yuqIAPqciRfMdbdqBg9HlswM6lCLi42oliz8NN0QUbYmEzhC2pRn6GgKUnVXX5QLxXxpnvvT9G_iBszK7VwuMaUb0AqAe35U7BvII_c9op_kYvlHc3GAYslS7Nv_T3izxVkABkGvI836HbSwgjPmXoxIiuFrUMe1k7Wknybp0BSCS42k_K6Wlx5KKAkmuajD7y4rZwx8PTrPk8_Sd-ynrfF4jYf_0Zl7qn9UvTgUgafUnScmLQaGgnyFrcHPzwkPNzVFp9hY1i6Erv3XF6vRo9rMzYl1WQ-mRXIVPyLA8h9_yBHv_o5A4ZK5iWl05zAy7xQiAu3ThUDUjrpFC51L5FRMSSpQrhYhOOJ9Dz4ZcPXPf6cz4JqWzDL4YK0aWyf6DoAQiEpLltVWdRiAZvThbUv3-AGlvPKAsXM7gjbUc8UNSIF4iCczDg4uY5YpWeMq3IdZV1EI5u2X5IbdCkZH_8lWazloJAgIBBS3DnC6_qbUwrraoNV42qWaHp6DwDlpTv16KG9S76nlxn1l70R9TDzLhjDQmrURVn-wbNAKGkU","DataProtected":true} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/keys/is-signing-key-ECD00E47D2EA2F8A964066AA9140BC2A.json b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/keys/is-signing-key-ECD00E47D2EA2F8A964066AA9140BC2A.json new file mode 100644 index 00000000..29646a72 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/keys/is-signing-key-ECD00E47D2EA2F8A964066AA9140BC2A.json @@ -0,0 +1 @@ +{"Version":1,"Id":"ECD00E47D2EA2F8A964066AA9140BC2A","Created":"2023-01-30T19:22:52.607245Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Bw3CDruPKBLkcUm6emGGavAofgT4wdgBVshrBXUFvBXLC39nCziU_P4l1k0yGoLHyLhouQwKySpNCEJfPV-FYe2wA7qtFa9k0y9zGzNlBirCiRxZhMAqePdo8QQb108kBRqymE0YsLDdoFd67ue8ZXUb6ano76ZSEwDwvfA_k48Yb2bmBSs8RdP5k8meilsLbtsw-TNlsli5Tizwy4FyPeQYhxw2K__KbCiBObC-aMpe0k_BkAWtlJMMvc8Bgm3ovBHAln2hHdXs5tG9KKGq50NHc1EMktoXotldScc1W-BwNb_iMvCSNbuqwZbmlCioc04YSC6Q0b8P7gU8-1zKcUW-nu0Jh0pEu9R-55dfANg9nQh4Pw8Mg6AaIEQ_RYAS7irE6_ZV2P8IgTL9FOVh9ITBNaeHoMS8O1LeIk-8DJb-BZK7AJXrpmbTYCuGp-dmKkErNQtp_RSpqwRUhdx-ystUlyckIaKIfvoKXAn52bTatH9RIh87AY0D47dr1AoA4nqIbBjr-84bJnbrhTnoYeFoD9rIspScF5IqpYvydzK0qpQjsUnMoBIM1c0wGGqn3lE9cpxpxl8___QjClssID2hCKhIplnCnh_GlBkXxjQ1Hd4EmWTrUOwCv1iCle29P6k13vnfDeSizbxEcMjbSk6hCGjlnO9E50lqY42IPTI9HmHZ6Ydk-TkqbqpbGooOR2FkqF4JQTGfC6Q2EIqyh6Yb5GS7c0_vCZWcQ0MA1t6DI7_vtdwRfO5HWzmRt8D16wnn_eRuOE5s1ep2JBDAFlMOgBVYNa4cYpiIqFl4DWBFHvoH7w8kfGUQWRTaV7Oxx32o-oigYuGzaN1L04SEGp0CAUcbYFXeBBLq3brJNCoHiMv-iqr_FYX6Posz2JHtaJ8NVb1KhJIBWs7F3MG30iyXCxc_9oBoXIzTsQvqFChyCUQzxfLn6M24IfheGj-F8E016RXSMnF3OiGKLKHwES1EDUIYbFwxlIcB7WAzUVxPB82PkUBcdXJX1co_HZEoViAI39QGIBYqHgPJafaTVLO-5fNYjexIFU1Eq8iVKDPLus1VFmjPFPI6gQ9H0_f-Xggk3SunZ9FSTTXOy8CXAJSxIcG6mZI01K7u8bG-_TM4EqtEN3KyBx7hSLFgF4P8ZlnCLc4BwV6KbheABXuG5kHBum_rZ8FlyfpdMlBHmKfRfl-ywq255ECUaxQCN9WoTokQnJcCUwoFLNpAmgc4GUFnNVWXEnL1_uwEtFmqolB95PaF0WgHuwAsWWSg2ILoJfc7XGUH8Cv5GOyEOnBJ9SVAhJLScvyZ4YnHVQvn9poxTcaPj83by2R4JqBCKI4zHNd_VA-O66SEuWjbTqK8g3tOOaTanj3j-0ZuI8SRp_WwjoU_Q7eMpNzI22zB_AJgElpOrIKiat1m9A1jIx5-p06gJQlVM8HRgdG-CQf_jgVpLr0kcgOLal8BXlTiFy6dAgw63mRAqD2CXzxebfWa7aaRh_Z-I7kK_x_awLZQc8-NoqiLd39FKIzrFzw5WBsz5ZkPvSJUBVHFUJb2blrgO22KYwpudEYVxDDWs44xmiPQmyedKkhnA12Bzwp4EKLlMcWPAR2QK1T5VxHZvpSSCVg0VJDPK9WLafKJJUpqvyjOFn0Ci_O2GnSa9Q3X-p4N6KdX3tRIbz_ORCiYLFgAQJxGVZx4LWEVzLb9OU8WXHo5lpj5TC6ANegQVdxvzzceyAENX9SL1mOZ2YgI5QLQGaUQ5mtWIKiLnR5NakFnAbl5eKiDSEesWFjelRK06iHEcBwPt4vbWcpOkI3AX3GJWAtUUQiP5oI40_Xs7Fsq1f2mCfzZa_Bw1P5ig0C0rAFzP8REyz8C81g06LJY4tkVUPoW05OsiEp6FE0U_6IK8QzSzDTG0mMW1wHdp4qyYQwi2ZUfu3-4eUKSc42h17URkbugl1H1C23CsNGwWzkyirY_YAAsRLP4YOpPnBInyU_4hpjGWSCYi9ZJaOqUxzJxpjqm4mrGGqY_01esl8R19Ckwa8amKwqdinTRxrE8xiTgbjzWs-rQnLKVVTYqqUmf4m4YJ2tpQ_GrNVmrQi270rdnsqbDEqjHkKVJu9NwCrzu2XnJza07uRLC-7rMmeNRsKjv48qHtGF6gy04ekrfZbExOfm4SdWOUFOLbO9hY4flHHilq5Fqlghq06E7cl-9Yx56N9p0_ZczoL5c9DfqdGQUCpxJ8a9ES6z7IRqBRid-VY3Ch6tXG4yPevSnTYXQjiReSVpgwNerI38ropT9gzie-v0K9wSfTDEY2QQCZ2Iz-ux5VC2tCP1Ml4NZzqyV1dMeyaRtRicDBgugSHsgWMm5JjuQIckFT9eJHG01HKiqAbSYmu_FSYaKTyM4WIX06R0cENpBXQMZBSiFmE9VvLRVTWO2nZiptJuRcr8NUR2xWo-3Z1sKNHnBuybF6gzxuyrXkLv4HZZUA5pNBOEUi7K","DataProtected":true} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/temp-keys/key-b40e9000-7740-49d3-ab06-339bfe93672d.xml b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/temp-keys/key-b40e9000-7740-49d3-ab06-339bfe93672d.xml new file mode 100644 index 00000000..ba20ba18 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/temp-keys/key-b40e9000-7740-49d3-ab06-339bfe93672d.xml @@ -0,0 +1,16 @@ + + + 2023-01-31T13:04:35.237094Z + 2023-01-31T13:04:35.232751Z + 2023-03-02T13:04:35.232751Z + + + + + + + mHhVTuGK7wh0O4d7IaHjPLF+2dsrzNuiQ+CG9Z4uD8DDNn3CEy2foaiuasdVIpIr9B4DC2Rssaylu5lwO4vVXA== + + + + \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/tempkey.jwk b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/tempkey.jwk new file mode 100644 index 00000000..eca46275 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/tempkey.jwk @@ -0,0 +1 @@ +{"AdditionalData":{},"Alg":"RS256","Crv":null,"D":"g1_v1TtTZLSTeqx7Km5djI9sexjD9lqTRE-UDVoXPHqMdyXviOHzeS54yX5DHx-LcUc2yzMCOzpdW--mWGuh_CWeVPm6-9my_-sz7MUvVYwHtIJv7HV_P_Z7Mb9wd-dhRJ8pboubeTqW1HPU4GjBPalxEq6hnhnATTiLihRqtVzoMMQwzkvaywt4mtAjz7OrblyWiYJJVh3lk2mOcooUVrgsZLuaYyKa2GHTsE7orGJFZ84utdrGQoQwVpa-DLKmbOSn1blLH-eV2DTrsntan0afj9qNO7BJ0a7VrdlQdmm8CZ6IOKHcwglIZxz3yHQiTDX7nbCZbiW4baw4VruC1Q","DP":"aJgzW6MYD_sECluoao9morRwtO4HqNICp0nv7RVB3RvoGzh0VsbGPPHO905KlQxHTBZW00oLPxGVKZ08fiGGVfCufjImYiApscvkA0X2OY6vjT3HRikCcADjk0TChe_MBS8RIPVpRqZxScrJ7FZWDfWeYN54x5GUlTiyx4lwyP0","DQ":"A6rHRq2QEplcBrN9y3pEpL-h6UctdMumnck2ZcfbLHSoSEmqyPaEdw_Nm9jxji0y0Q78RSKP5_hujtXkgl8hfaw0lD-mCQbuDQE0wIR3P7L_hLJqemBPhCuvWc7dewJvIn9qHNKCLeSn_McUHFsx_sa-hPgY94qovAhmmEfltes","E":"AQAB","K":null,"KeyId":"A2BF81A404CA894A198823B740BB89D7","KeyOps":[],"Kid":"A2BF81A404CA894A198823B740BB89D7","Kty":"RSA","N":"ofvEefG8t3Y3c3rZJExPItJVCfEm1FfMjoSYFWAXsHGNpALJf3_A8OaTHjosjqvUGUT2-LFXrn33y2RKdcl2W_DjiKcbCpqbWi0guf2yxQMLjBeLSVW0UuztovjApfL99nTJGQaOGBTsFvUlSyz1VXzO1MW9JNO8RzBTUKrxgbsjKPJoK97ztP6tVkEpM6nB-UeF-cM6XrZSl36D3MIVnXlYqi9CVE65CfcnkuzEmersTDNRdTEQ1J9vJ-5SnTCx37WCfMc4xMnz4njolDhQWlrhRLNvBWvuEcgE-Bjuq1OILbInciw576Rvy9-rGBhS6zoBA5ooBq-96UHGRbWPKQ","Oth":null,"P":"07ZHSN7upUW4eM99v5NXUkTQff1ZGSwJHmHAQ9kXV9s6ZCpQClW3MZs8z4LGOqhx69XVWU3WOvVL2X98podtkMMbVQheqPfrA_tNTd0w8CWEh6f83lX_N6QvaMPgZRirEzSxK96_JojY1SGJnYyNBkZ3YLRYyp4R7DZFW8winlc","Q":"w95ls9ArbkBkKMrmiiHTJWdp-EyD2q0MMTP9SUkDnKcQrf6OAYYJF6HVyDMzWanUqB8n4AsRAshnVQKTWq7PjgUPU0tgvaVCH_mwqNWY939Qrf290W3uKE8u1oah4K-0P_VHMf6KM-qxpGvX0soJLJgo1VuURFsGMeSXMmJ5zn8","QI":"hXSMhu4LYi5AuuL2JgRSeDH8EDVhOmjvewEufg8AiAa5_ArsPqY-eQY1KY4JsHz6A5A0zc6HaHpCtt47o9_w3MEp_k69-RdXwPmNuwi809y2t8LOcD2B2wX2Q4YpLdwBH8dUM178a8u_o_xj4yBuK3iGXBzN11xwyi5pFG78Feg","Use":null,"X":null,"X5c":[],"X5t":null,"X5tS256":null,"X5u":null,"Y":null,"KeySize":2048,"HasPrivateKey":true,"CryptoProviderFactory":{"CryptoProviderCache":{},"CustomCryptoProvider":null,"CacheSignatureProviders":true,"SignatureProviderObjectPoolCacheSize":24}} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.css b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.css new file mode 100644 index 00000000..0bde1d3e --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.css @@ -0,0 +1,30 @@ +.welcome-page .logo { + width: 64px; } + +.icon-banner { + width: 32px; } + +.body-container { + margin-top: 60px; + padding-bottom: 40px; } + +.welcome-page li { + list-style: none; + padding: 4px; } + +.logged-out-page iframe { + display: none; + width: 0; + height: 0; } + +.grants-page .card { + margin-top: 20px; + border-bottom: 1px solid lightgray; } + .grants-page .card .card-title { + font-size: 120%; + font-weight: bold; } + .grants-page .card .card-title img { + width: 100px; + height: 100px; } + .grants-page .card label { + font-weight: bold; } diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.min.css b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.min.css new file mode 100644 index 00000000..f3e19857 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.min.css @@ -0,0 +1 @@ +.welcome-page .logo{width:64px;}.icon-banner{width:32px;}.body-container{margin-top:60px;padding-bottom:40px;}.welcome-page li{list-style:none;padding:4px;}.logged-out-page iframe{display:none;width:0;height:0;}.grants-page .card{margin-top:20px;border-bottom:1px solid #d3d3d3;}.grants-page .card .card-title{font-size:120%;font-weight:bold;}.grants-page .card .card-title img{width:100px;height:100px;}.grants-page .card label{font-weight:bold;} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.scss b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.scss new file mode 100644 index 00000000..72c34854 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/css/site.scss @@ -0,0 +1,51 @@ +.welcome-page { + .logo{ + width:64px; + } +} +.icon-banner { + width: 32px; +} + +.body-container { + margin-top: 60px; + padding-bottom: 40px; +} + +.welcome-page { + li { + list-style: none; + padding: 4px; + } +} + +.logged-out-page { + iframe { + display: none; + width: 0; + height: 0; + } +} + +.grants-page { + .card { + margin-top: 20px; + border-bottom: 1px solid lightgray; + + .card-title { + img { + width: 100px; + height: 100px; + } + + font-size: 120%; + font-weight: bold; + } + + label { + font-weight: bold; + } + } +} + + diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/duende-logo.svg b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/duende-logo.svg new file mode 100644 index 00000000..5fa55bf3 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/duende-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/favicon.ico b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/favicon.ico new file mode 100644 index 00000000..f7ecfd99 Binary files /dev/null and b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/favicon.ico differ diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/js/signin-redirect.js b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/js/signin-redirect.js new file mode 100644 index 00000000..6ebc5691 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/js/signin-redirect.js @@ -0,0 +1 @@ +window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/js/signout-redirect.js b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/js/signout-redirect.js new file mode 100644 index 00000000..cdfc5e78 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/js/signout-redirect.js @@ -0,0 +1,6 @@ +window.addEventListener("load", function () { + var a = document.querySelector("a.PostLogoutRedirectUri"); + if (a) { + window.location = a.href; + } +}); diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/README.md b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/README.md new file mode 100644 index 00000000..35ce9fea --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/README.md @@ -0,0 +1,209 @@ +

    + + Bootstrap logo + +

    + +

    Bootstrap

    + +

    + Sleek, intuitive, and powerful front-end framework for faster and easier web development. +
    + Explore Bootstrap docs » +
    +
    + Report bug + · + Request feature + · + Themes + · + Blog +

    + + +## Table of contents + +- [Quick start](#quick-start) +- [Status](#status) +- [What's included](#whats-included) +- [Bugs and feature requests](#bugs-and-feature-requests) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Community](#community) +- [Versioning](#versioning) +- [Creators](#creators) +- [Thanks](#thanks) +- [Copyright and license](#copyright-and-license) + + +## Quick start + +Several quick start options are available: + +- [Download the latest release.](https://github.com/twbs/bootstrap/archive/v4.5.3.zip) +- Clone the repo: `git clone https://github.com/twbs/bootstrap.git` +- Install with [npm](https://www.npmjs.com/): `npm install bootstrap` +- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@4.5.3` +- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:4.5.3` +- Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass` + +Read the [Getting started page](https://getbootstrap.com/docs/4.5/getting-started/introduction/) for information on the framework contents, templates and examples, and more. + + +## Status + +[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/) +[![Build Status](https://github.com/twbs/bootstrap/workflows/JS%20Tests/badge.svg?branch=v4-dev)](https://github.com/twbs/bootstrap/actions?query=workflow%3AJS+Tests+branch%3Av4-dev) +[![npm version](https://img.shields.io/npm/v/bootstrap)](https://www.npmjs.com/package/bootstrap) +[![Gem version](https://img.shields.io/gem/v/bootstrap)](https://rubygems.org/gems/bootstrap) +[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue)](https://atmospherejs.com/twbs/bootstrap) +[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap)](https://packagist.org/packages/twbs/bootstrap) +[![NuGet](https://img.shields.io/nuget/vpre/bootstrap)](https://www.nuget.org/packages/bootstrap/absoluteLatest) +[![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=peer) +[![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=dev) +[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/v4-dev)](https://coveralls.io/github/twbs/bootstrap?branch=v4-dev) +[![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/css/bootstrap.min.css?compression=gzip&label=CSS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/v4-dev/dist/css/bootstrap.min.css) +[![JS gzip size](https://img.badgesize.io/twbs/bootstrap/v4-dev/dist/js/bootstrap.min.js?compression=gzip&label=JS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/v4-dev/dist/js/bootstrap.min.js) +[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)](https://www.browserstack.com/automate/public-build/SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229) +[![Backers on Open Collective](https://img.shields.io/opencollective/backers/bootstrap)](#backers) +[![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/bootstrap)](#sponsors) + + +## What's included + +Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: + +```text +bootstrap/ +└── dist/ + ├── css/ + │ ├── bootstrap-grid.css + │ ├── bootstrap-grid.css.map + │ ├── bootstrap-grid.min.css + │ ├── bootstrap-grid.min.css.map + │ ├── bootstrap-reboot.css + │ ├── bootstrap-reboot.css.map + │ ├── bootstrap-reboot.min.css + │ ├── bootstrap-reboot.min.css.map + │ ├── bootstrap.css + │ ├── bootstrap.css.map + │ ├── bootstrap.min.css + │ └── bootstrap.min.css.map + └── js/ + ├── bootstrap.bundle.js + ├── bootstrap.bundle.js.map + ├── bootstrap.bundle.min.js + ├── bootstrap.bundle.min.js.map + ├── bootstrap.js + ├── bootstrap.js.map + ├── bootstrap.min.js + └── bootstrap.min.js.map +``` + +We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/), but not [jQuery](https://jquery.com/). + + +## Bugs and feature requests + +Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/v4-dev/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new). + + +## Documentation + +Bootstrap's documentation, included in this repo in the root directory, is built with [Jekyll](https://jekyllrb.com/) and publicly hosted on GitHub Pages at . The docs may also be run locally. + +Documentation search is powered by [Algolia's DocSearch](https://community.algolia.com/docsearch/). Working on our search? Be sure to set `debug: true` in `site/docs/4.5/assets/js/src/search.js` file. + +### Running documentation locally + +1. Run through the [tooling setup](https://getbootstrap.com/docs/4.5/getting-started/build-tools/#tooling-setup) to install Jekyll (the site builder) and other Ruby dependencies with `bundle install`. +2. Run `npm install` to install Node.js dependencies. +3. Run `npm start` to compile CSS and JavaScript files, generate our docs, and watch for changes. +4. Open `http://localhost:9001` in your browser, and voilà. + +Learn more about using Jekyll by reading its [documentation](https://jekyllrb.com/docs/). + +### Documentation for previous releases + +You can find all our previous releases docs on . + +[Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download. + + +## Contributing + +Please read through our [contributing guidelines](https://github.com/twbs/bootstrap/blob/v4-dev/.github/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. + +Moreover, if your pull request contains JavaScript patches or features, you must include [relevant unit tests](https://github.com/twbs/bootstrap/tree/v4-dev/js/tests). All HTML and CSS should conform to the [Code Guide](https://github.com/mdo/code-guide), maintained by [Mark Otto](https://github.com/mdo). + +Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/v4-dev/.editorconfig) for easy use in common text editors. Read more and download plugins at . + + +## Community + +Get updates on Bootstrap's development and chat with the project maintainers and community members. + +- Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap). +- Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/). +- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). +- Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. +- Implementation help may be found at Stack Overflow (tagged [`bootstrap-4`](https://stackoverflow.com/questions/tagged/bootstrap-4)). +- Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability. + + +## Versioning + +For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible. + +See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release. + + +## Creators + +**Mark Otto** + +- +- + +**Jacob Thornton** + +- +- + + +## Thanks + + + BrowserStack Logo + + +Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers! + + +## Sponsors + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/bootstrap#sponsor)] + +[![](https://opencollective.com/bootstrap/sponsor/0/avatar.svg)](https://opencollective.com/bootstrap/sponsor/0/website) +[![](https://opencollective.com/bootstrap/sponsor/1/avatar.svg)](https://opencollective.com/bootstrap/sponsor/1/website) +[![](https://opencollective.com/bootstrap/sponsor/2/avatar.svg)](https://opencollective.com/bootstrap/sponsor/2/website) +[![](https://opencollective.com/bootstrap/sponsor/3/avatar.svg)](https://opencollective.com/bootstrap/sponsor/3/website) +[![](https://opencollective.com/bootstrap/sponsor/4/avatar.svg)](https://opencollective.com/bootstrap/sponsor/4/website) +[![](https://opencollective.com/bootstrap/sponsor/5/avatar.svg)](https://opencollective.com/bootstrap/sponsor/5/website) +[![](https://opencollective.com/bootstrap/sponsor/6/avatar.svg)](https://opencollective.com/bootstrap/sponsor/6/website) +[![](https://opencollective.com/bootstrap/sponsor/7/avatar.svg)](https://opencollective.com/bootstrap/sponsor/7/website) +[![](https://opencollective.com/bootstrap/sponsor/8/avatar.svg)](https://opencollective.com/bootstrap/sponsor/8/website) +[![](https://opencollective.com/bootstrap/sponsor/9/avatar.svg)](https://opencollective.com/bootstrap/sponsor/9/website) + + +## Backers + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/bootstrap#backer)] + +[![Backers](https://opencollective.com/bootstrap/backers.svg?width=890)](https://opencollective.com/bootstrap#backers) + + +## Copyright and license + +Code and documentation copyright 2011-2020 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css new file mode 100644 index 00000000..9cfa07ac --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css @@ -0,0 +1,3872 @@ +/*! + * Bootstrap Grid v4.5.3 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +html { + box-sizing: border-box; + -ms-overflow-style: scrollbar; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +.container, +.container-fluid, +.container-sm, +.container-md, +.container-lg, +.container-xl { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; + } +} + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.row-cols-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.row-cols-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.row-cols-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.row-cols-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.row-cols-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; +} + +.row-cols-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; +} + +.col-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; +} + +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} + +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} + +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; +} + +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; +} + +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; +} + +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; +} + +.col-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; +} + +.col-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + -ms-flex-order: -1; + order: -1; +} + +.order-last { + -ms-flex-order: 13; + order: 13; +} + +.order-0 { + -ms-flex-order: 0; + order: 0; +} + +.order-1 { + -ms-flex-order: 1; + order: 1; +} + +.order-2 { + -ms-flex-order: 2; + order: 2; +} + +.order-3 { + -ms-flex-order: 3; + order: 3; +} + +.order-4 { + -ms-flex-order: 4; + order: 4; +} + +.order-5 { + -ms-flex-order: 5; + order: 5; +} + +.order-6 { + -ms-flex-order: 6; + order: 6; +} + +.order-7 { + -ms-flex-order: 7; + order: 7; +} + +.order-8 { + -ms-flex-order: 8; + order: 8; +} + +.order-9 { + -ms-flex-order: 9; + order: 9; +} + +.order-10 { + -ms-flex-order: 10; + order: 10; +} + +.order-11 { + -ms-flex-order: 11; + order: 11; +} + +.order-12 { + -ms-flex-order: 12; + order: 12; +} + +.offset-1 { + margin-left: 8.333333%; +} + +.offset-2 { + margin-left: 16.666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.333333%; +} + +.offset-5 { + margin-left: 41.666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.333333%; +} + +.offset-8 { + margin-left: 66.666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.333333%; +} + +.offset-11 { + margin-left: 91.666667%; +} + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-sm-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-sm-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-sm-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-sm-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-sm-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-sm-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-sm-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-sm-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-sm-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-sm-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-sm-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-sm-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-sm-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-sm-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-sm-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.333333%; + } + .offset-sm-2 { + margin-left: 16.666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.333333%; + } + .offset-sm-5 { + margin-left: 41.666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.333333%; + } + .offset-sm-8 { + margin-left: 66.666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.333333%; + } + .offset-sm-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-md-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-md-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-md-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-md-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-md-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-md-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-md-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-md-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-md-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-md-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-md-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-md-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-md-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.333333%; + } + .offset-md-2 { + margin-left: 16.666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.333333%; + } + .offset-md-5 { + margin-left: 41.666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.333333%; + } + .offset-md-8 { + margin-left: 66.666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.333333%; + } + .offset-md-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-lg-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-lg-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-lg-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-lg-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-lg-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-lg-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-lg-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-lg-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-lg-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-lg-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-lg-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-lg-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-lg-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-lg-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-lg-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.333333%; + } + .offset-lg-2 { + margin-left: 16.666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.333333%; + } + .offset-lg-5 { + margin-left: 41.666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.333333%; + } + .offset-lg-8 { + margin-left: 66.666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.333333%; + } + .offset-lg-11 { + margin-left: 91.666667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .row-cols-xl-1 > * { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .row-cols-xl-2 > * { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .row-cols-xl-3 > * { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .row-cols-xl-4 > * { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .row-cols-xl-5 > * { + -ms-flex: 0 0 20%; + flex: 0 0 20%; + max-width: 20%; + } + .row-cols-xl-6 > * { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-auto { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: 100%; + } + .col-xl-1 { + -ms-flex: 0 0 8.333333%; + flex: 0 0 8.333333%; + max-width: 8.333333%; + } + .col-xl-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } + .col-xl-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-xl-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; + } + .col-xl-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -ms-flex: 0 0 58.333333%; + flex: 0 0 58.333333%; + max-width: 58.333333%; + } + .col-xl-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; + } + .col-xl-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } + .col-xl-11 { + -ms-flex: 0 0 91.666667%; + flex: 0 0 91.666667%; + max-width: 91.666667%; + } + .col-xl-12 { + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.333333%; + } + .offset-xl-2 { + margin-left: 16.666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.333333%; + } + .offset-xl-5 { + margin-left: 41.666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.333333%; + } + .offset-xl-8 { + margin-left: 66.666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.333333%; + } + .offset-xl-11 { + margin-left: 91.666667%; + } +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -ms-flexbox !important; + display: flex !important; +} + +.d-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +.flex-row { + -ms-flex-direction: row !important; + flex-direction: row !important; +} + +.flex-column { + -ms-flex-direction: column !important; + flex-direction: column !important; +} + +.flex-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} + +.flex-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} + +.flex-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} + +.justify-content-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} + +.justify-content-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.justify-content-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} + +.align-items-start { + -ms-flex-align: start !important; + align-items: flex-start !important; +} + +.align-items-end { + -ms-flex-align: end !important; + align-items: flex-end !important; +} + +.align-items-center { + -ms-flex-align: center !important; + align-items: center !important; +} + +.align-items-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; +} + +.align-items-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; +} + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-n1 { + margin: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.5rem !important; +} + +.m-n3 { + margin: -1rem !important; +} + +.mt-n3, +.my-n3 { + margin-top: -1rem !important; +} + +.mr-n3, +.mx-n3 { + margin-right: -1rem !important; +} + +.mb-n3, +.my-n3 { + margin-bottom: -1rem !important; +} + +.ml-n3, +.mx-n3 { + margin-left: -1rem !important; +} + +.m-n4 { + margin: -1.5rem !important; +} + +.mt-n4, +.my-n4 { + margin-top: -1.5rem !important; +} + +.mr-n4, +.mx-n4 { + margin-right: -1.5rem !important; +} + +.mb-n4, +.my-n4 { + margin-bottom: -1.5rem !important; +} + +.ml-n4, +.mx-n4 { + margin-left: -1.5rem !important; +} + +.m-n5 { + margin: -3rem !important; +} + +.mt-n5, +.my-n5 { + margin-top: -3rem !important; +} + +.mr-n5, +.mx-n5 { + margin-right: -3rem !important; +} + +.mb-n5, +.my-n5 { + margin-bottom: -3rem !important; +} + +.ml-n5, +.mx-n5 { + margin-left: -3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-n1 { + margin: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.5rem !important; + } + .m-sm-n3 { + margin: -1rem !important; + } + .mt-sm-n3, + .my-sm-n3 { + margin-top: -1rem !important; + } + .mr-sm-n3, + .mx-sm-n3 { + margin-right: -1rem !important; + } + .mb-sm-n3, + .my-sm-n3 { + margin-bottom: -1rem !important; + } + .ml-sm-n3, + .mx-sm-n3 { + margin-left: -1rem !important; + } + .m-sm-n4 { + margin: -1.5rem !important; + } + .mt-sm-n4, + .my-sm-n4 { + margin-top: -1.5rem !important; + } + .mr-sm-n4, + .mx-sm-n4 { + margin-right: -1.5rem !important; + } + .mb-sm-n4, + .my-sm-n4 { + margin-bottom: -1.5rem !important; + } + .ml-sm-n4, + .mx-sm-n4 { + margin-left: -1.5rem !important; + } + .m-sm-n5 { + margin: -3rem !important; + } + .mt-sm-n5, + .my-sm-n5 { + margin-top: -3rem !important; + } + .mr-sm-n5, + .mx-sm-n5 { + margin-right: -3rem !important; + } + .mb-sm-n5, + .my-sm-n5 { + margin-bottom: -3rem !important; + } + .ml-sm-n5, + .mx-sm-n5 { + margin-left: -3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-n1 { + margin: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.5rem !important; + } + .m-md-n3 { + margin: -1rem !important; + } + .mt-md-n3, + .my-md-n3 { + margin-top: -1rem !important; + } + .mr-md-n3, + .mx-md-n3 { + margin-right: -1rem !important; + } + .mb-md-n3, + .my-md-n3 { + margin-bottom: -1rem !important; + } + .ml-md-n3, + .mx-md-n3 { + margin-left: -1rem !important; + } + .m-md-n4 { + margin: -1.5rem !important; + } + .mt-md-n4, + .my-md-n4 { + margin-top: -1.5rem !important; + } + .mr-md-n4, + .mx-md-n4 { + margin-right: -1.5rem !important; + } + .mb-md-n4, + .my-md-n4 { + margin-bottom: -1.5rem !important; + } + .ml-md-n4, + .mx-md-n4 { + margin-left: -1.5rem !important; + } + .m-md-n5 { + margin: -3rem !important; + } + .mt-md-n5, + .my-md-n5 { + margin-top: -3rem !important; + } + .mr-md-n5, + .mx-md-n5 { + margin-right: -3rem !important; + } + .mb-md-n5, + .my-md-n5 { + margin-bottom: -3rem !important; + } + .ml-md-n5, + .mx-md-n5 { + margin-left: -3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-n1 { + margin: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.5rem !important; + } + .m-lg-n3 { + margin: -1rem !important; + } + .mt-lg-n3, + .my-lg-n3 { + margin-top: -1rem !important; + } + .mr-lg-n3, + .mx-lg-n3 { + margin-right: -1rem !important; + } + .mb-lg-n3, + .my-lg-n3 { + margin-bottom: -1rem !important; + } + .ml-lg-n3, + .mx-lg-n3 { + margin-left: -1rem !important; + } + .m-lg-n4 { + margin: -1.5rem !important; + } + .mt-lg-n4, + .my-lg-n4 { + margin-top: -1.5rem !important; + } + .mr-lg-n4, + .mx-lg-n4 { + margin-right: -1.5rem !important; + } + .mb-lg-n4, + .my-lg-n4 { + margin-bottom: -1.5rem !important; + } + .ml-lg-n4, + .mx-lg-n4 { + margin-left: -1.5rem !important; + } + .m-lg-n5 { + margin: -3rem !important; + } + .mt-lg-n5, + .my-lg-n5 { + margin-top: -3rem !important; + } + .mr-lg-n5, + .mx-lg-n5 { + margin-right: -3rem !important; + } + .mb-lg-n5, + .my-lg-n5 { + margin-bottom: -3rem !important; + } + .ml-lg-n5, + .mx-lg-n5 { + margin-left: -3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-n1 { + margin: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.5rem !important; + } + .m-xl-n3 { + margin: -1rem !important; + } + .mt-xl-n3, + .my-xl-n3 { + margin-top: -1rem !important; + } + .mr-xl-n3, + .mx-xl-n3 { + margin-right: -1rem !important; + } + .mb-xl-n3, + .my-xl-n3 { + margin-bottom: -1rem !important; + } + .ml-xl-n3, + .mx-xl-n3 { + margin-left: -1rem !important; + } + .m-xl-n4 { + margin: -1.5rem !important; + } + .mt-xl-n4, + .my-xl-n4 { + margin-top: -1.5rem !important; + } + .mr-xl-n4, + .mx-xl-n4 { + margin-right: -1.5rem !important; + } + .mb-xl-n4, + .my-xl-n4 { + margin-bottom: -1.5rem !important; + } + .ml-xl-n4, + .mx-xl-n4 { + margin-left: -1.5rem !important; + } + .m-xl-n5 { + margin: -3rem !important; + } + .mt-xl-n5, + .my-xl-n5 { + margin-top: -3rem !important; + } + .mr-xl-n5, + .mx-xl-n5 { + margin-right: -3rem !important; + } + .mb-xl-n5, + .my-xl-n5 { + margin-bottom: -3rem !important; + } + .ml-xl-n5, + .mx-xl-n5 { + margin-left: -3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map new file mode 100644 index 00000000..a664f980 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"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EKp0BM;IAAwB,iBADZ;IACY,QADZ;ELw0BlB;EKv0BM;IAAwB,iBADZ;IACY,QADZ;EL20BlB;EK10BM;IAAwB,kBADZ;IACY,SADZ;EL80BlB;EK70BM;IAAwB,kBADZ;IACY,SADZ;ELi1BlB;EKh1BM;IAAwB,kBADZ;IACY,SADZ;ELo1BlB;EK50BU;IHhBV,cAA4B;EF+1B5B;EK/0BU;IHhBV,sBAA8C;EFk2B9C;EKl1BU;IHhBV,uBAA8C;EFq2B9C;EKr1BU;IHhBV,gBAA8C;EFw2B9C;EKx1BU;IHhBV,uBAA8C;EF22B9C;EK31BU;IHhBV,uBAA8C;EF82B9C;EK91BU;IHhBV,gBAA8C;EFi3B9C;EKj2BU;IHhBV,uBAA8C;EFo3B9C;EKp2BU;IHhBV,uBAA8C;EFu3B9C;EKv2BU;IHhBV,gBAA8C;EF03B9C;EK12BU;IHhBV,uBAA8C;EF63B9C;EK72BU;IHhBV,uBAA8C;EFg4B9C;AACF;;AM76BM;EAAwB,wBAA0B;ANi7BxD;;AMj7BM;EAAwB,0BAA0B;ANq7BxD;;AMr7BM;EAAwB,gCAA0B;ANy7BxD;;AMz7BM;EAAwB,yBAA0B;AN67BxD;;AM77BM;EAAwB,yBAA0B;ANi8BxD;;AMj8BM;EAAwB,6BAA0B;ANq8BxD;;AMr8BM;EAAwB,8BAA0B;ANy8BxD;;AMz8BM;EAAwB,+BAA0B;EAA1B,wBAA0B;AN68BxD;;AM78BM;EAAwB,sCAA0B;EAA1B,+BAA0B;ANi9BxD;;AGh6BI;EGjDE;IAAwB,wBAA0B;ENs9BtD;EMt9BI;IAAwB,0BAA0B;ENy9BtD;EMz9BI;IAAwB,gCAA0B;EN49BtD;EM59BI;IAAwB,yBAA0B;EN+9BtD;EM/9BI;IAAwB,yBAA0B;ENk+BtD;EMl+BI;IAAwB,6BAA0B;ENq+BtD;EMr+BI;IAAwB,8BAA0B;ENw+BtD;EMx+BI;IAAwB,+BAA0B;IAA1B,wBAA0B;EN2+BtD;EM3+BI;IAAwB,sCAA0B;IAA1B,+BAA0B;EN8+BtD;AACF;;AG97BI;EGjDE;IAAwB,wBAA0B;ENo/BtD;EMp/BI;IAAwB,0BAA0B;ENu/BtD;EMv/BI;IAAwB,gCAA0B;EN0/BtD;EM1/BI;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hD;;AQpsDQ;EAAgC,yBAA4B;ARwsDpE;;AQvsDQ;;EAEE,6BAAoC;AR0sD9C;;AQxsDQ;;EAEE,+BAAwC;AR2sDlD;;AQzsDQ;;EAEE,gCAA0C;AR4sDpD;;AQ1sDQ;;EAEE,8BAAsC;AR6sDhD;;AQ5tDQ;EAAgC,uBAA4B;ARguDpE;;AQ/tDQ;;EAEE,2BAAoC;ARkuD9C;;AQhuDQ;;EAEE,6BAAwC;ARmuDlD;;AQjuDQ;;EAEE,8BAA0C;ARouDpD;;AQluDQ;;EAEE,4BAAsC;ARquDhD;;AQpvDQ;EAAgC,yBAA4B;ARwvDpE;;AQvvDQ;;EAEE,6BAAoC;AR0vD9C;;AQxvDQ;;EAEE,+BAAwC;AR2vDlD;;AQzvDQ;;EAEE,gCAA0C;AR4vDpD;;AQ1vDQ;;EAEE,8BAAsC;AR6vDhD;;AQ5wDQ;EAAgC,uBAA4B;ARgxDpE;;AQ/wDQ;;EAEE,2BAAoC;ARkxD9C;;AQhxDQ;;EAEE,6BAAwC;ARmxDlD;;AQjxDQ;;EAEE,8BAA0C;ARoxDpD;;AQlxDQ;;EAEE,4BAAsC;ARqxDhD;;AQpyDQ;EAAgC,qBAA4B;ARwyDpE;;AQvyDQ;;EAEE,yBAAoC;AR0yD9C;;AQxyDQ;;EAEE,2BAAwC;AR2yDlD;;AQzyDQ;;EAEE,4BAA0C;AR4yDpD;;AQ1yDQ;;EAEE,0BAAsC;AR6yDhD;;AQ5zDQ;EAAgC,2BAA4B;ARg0DpE;;AQ/zDQ;;EAEE,+BAAoC;ARk0D9C;;AQh0DQ;;EAEE,iCAAwC;ARm0DlD;;AQj0DQ;;EAEE,kCAA0C;ARo0DpD;;AQl0DQ;;EAEE,gCAAsC;ARq0DhD;;AQp1DQ;EAAgC,0BAA4B;ARw1DpE;;AQv1DQ;;EAEE,8BAAoC;AR01D9C;;AQx1DQ;;EAEE,gCAAwC;AR21DlD;;AQz1DQ;;EAEE,iCAA0C;AR41DpD;;AQ11DQ;;EAEE,+BAAsC;AR61DhD;;AQ52DQ;EAAgC,wBAA4B;ARg3DpE;;AQ/2DQ;;EAEE,4BAAoC;ARk3D9C;;AQh3DQ;;EAEE,8BAAwC;ARm3DlD;;AQj3DQ;;EAEE,+BAA0C;ARo3DpD;;AQl3DQ;;EAEE,6BAAsC;ARq3DhD;;AQp4DQ;EAAgC,0BAA4B;ARw4DpE;;AQv4DQ;;EAEE,8BAAoC;AR04D9C;;AQx4DQ;;EAEE,gCAAwC;AR24DlD;;AQz4DQ;;EAEE,iCAA0C;AR44DpD;;AQ14DQ;;EAEE,+BAAsC;AR64DhD;;AQ55DQ;EAAgC,wBAA4B;ARg6DpE;;AQ/5DQ;;EAEE,4BAAoC;ARk6D9C;;AQh6DQ;;EAEE,8BAAwC;ARm6DlD;;AQj6DQ;;EAEE,+BAA0C;ARo6DpD;;AQl6DQ;;EAEE,6BAAsC;ARq6DhD;;AQ75DQ;EAAwB,2BAA2B;ARi6D3D;;AQh6DQ;;EAEE,+BAA+B;ARm6DzC;;AQj6DQ;;EAEE,iCAAiC;ARo6D3C;;AQl6DQ;;EAEE,kCAAkC;ARq6D5C;;AQn6DQ;;EAEE,gCAAgC;ARs6D1C;;AQr7DQ;EAAwB,0BAA2B;ARy7D3D;;AQx7DQ;;EAEE,8BAA+B;AR27DzC;;AQz7DQ;;EAEE,gCAAiC;AR47D3C;;AQ17DQ;;EAEE,iCAAkC;AR67D5C;;AQ37DQ;;EAEE,+BAAgC;AR87D1C;;AQ78DQ;EAAwB,wBAA2B;ARi9D3D;;AQh9DQ;;EAEE,4BAA+B;ARm9DzC;;AQj9DQ;;EAEE,8BAAiC;ARo9D3C;;AQl9DQ;;EAEE,+BAAkC;ARq9D5C;;AQn9DQ;;EAEE,6BAAgC;ARs9D1C;;AQr+DQ;EAAwB,0BAA2B;ARy+D3D;;AQx+DQ;;EAEE,8BAA+B;AR2+DzC;;AQz+DQ;;EAEE,gCAAiC;AR4+D3C;;AQ1+DQ;;EAEE,iCAAkC;AR6+D5C;;AQ3+DQ;;EAEE,+BAAgC;AR8+D1C;;AQ7/DQ;EAAwB,wBAA2B;ARigE3D;;AQhgEQ;;EAEE,4BAA+B;ARmgEzC;;AQjgEQ;;EAEE,8BAAiC;ARogE3C;;AQlgEQ;;EAEE,+BAAkC;ARqgE5C;;AQngEQ;;EAEE,6BAAgC;ARsgE1C;;AQhgEI;EAAmB,uBAAuB;ARogE9C;;AQngEI;;EAEE,2BAA2B;ARsgEjC;;AQpgEI;;EAEE,6BAA6B;ARugEnC;;AQrgEI;;EAEE,8BAA8B;ARwgEpC;;AQtgEI;;EAEE,4BAA4B;ARygElC;;AGlhEI;EKlDI;IAAgC,oBAA4B;ERykElE;EQxkEM;;IAEE,wBAAoC;ER0kE5C;EQxkEM;;IAEE,0BAAwC;ER0kEhD;EQxkEM;;IAEE,2BAA0C;ER0kElD;EQxkEM;;IAEE,yBAAsC;ER0kE9C;EQzlEM;IAAgC,0BAA4B;ER4lElE;EQ3lEM;;IAEE,8BAAoC;ER6lE5C;EQ3lEM;;IAEE,gCAAwC;ER6lEhD;EQ3lEM;;IAEE,iCAA0C;ER6lElD;EQ3lEM;;IAEE,+BAAsC;ER6lE9C;EQ5mEM;IAAgC,yBAA4B;ER+mElE;EQ9mEM;;IAEE,6BAAoC;ERgnE5C;EQ9mEM;;IAEE,+BAAwC;ERgnEhD;EQ9mEM;;IAEE,gCAA0C;ERgnElD;EQ9mEM;;IAEE,8BAAsC;ERgnE9C;EQ/nEM;IAAgC,uBAA4B;ERkoElE;EQjoEM;;IAEE,2BAAoC;ERmoE5C;EQjoEM;;IAEE,6BAAwC;ERmoEhD;EQjoEM;;IAEE,8BAA0C;ERmoElD;EQjoEM;;IAEE,4BAAsC;ERmoE9C;EQlpEM;IAAgC,yBAA4B;ERqpElE;EQppEM;;IAEE,6BAAoC;ERspE5C;EQppEM;;IAEE,+BAAwC;ERspEhD;EQppEM;;IAEE,gCAA0C;ERspElD;EQppEM;;IAEE,8BAAsC;ERspE9C;EQrqEM;IAAgC,uBAA4B;ERwqElE;EQvqEM;;IAEE,2BAAoC;ERyqE5C;EQvqEM;;IAEE,6BAAwC;ERyqEhD;EQvqEM;;IAEE,8BAA0C;ERyqElD;EQvqEM;;IAEE,4BAAsC;ERyqE9C;EQxrEM;IAAgC,qBAA4B;ER2rElE;EQ1rEM;;IAEE,yBAAoC;ER4rE5C;EQ1rEM;;IAEE,2BAAwC;ER4rEhD;EQ1rEM;;IAEE,4BAA0C;ER4rElD;EQ1rEM;;IAEE,0BAAsC;ER4rE9C;EQ3sEM;IAAgC,2BAA4B;ER8sElE;EQ7sEM;;IAEE,+BAAoC;ER+sE5C;EQ7sEM;;IAEE,iCAAwC;ER+sEhD;EQ7sEM;;IAEE,kCAA0C;ER+sElD;EQ7sEM;;IAEE,gCAAsC;ER+sE9C;EQ9tEM;IAAgC,0BAA4B;ERiuElE;EQhuEM;;IAEE,8BAAoC;ERkuE5C;EQhuEM;;IAEE,gCAAwC;ERkuEhD;EQhuEM;;IAEE,iCAA0C;ERkuElD;EQhuEM;;IAEE,+BAAsC;ERkuE9C;EQjvEM;IAAgC,wBAA4B;ERovElE;EQnvEM;;IAEE,4BAAoC;ERqvE5C;EQnvEM;;IAEE,8BAAwC;ERqvEhD;EQnvEM;;IAEE,+BAA0C;ERqvElD;EQnvEM;;IAEE,6BAAsC;ERqvE9C;EQpwEM;IAAgC,0BAA4B;ERuwElE;EQtwEM;;IAEE,8BAAoC;ERwwE5C;EQtwEM;;IAEE,gCAAwC;ERwwEhD;EQtwEM;;IAEE,iCAA0C;ERwwElD;EQtwEM;;IAEE,+BAAsC;ERwwE9C;EQvxEM;IAAgC,wBAA4B;ER0xElE;EQzxEM;;IAEE,4BAAoC;ER2xE5C;EQzxEM;;IAEE,8BAAwC;ER2xEhD;EQzxEM;;IAEE,+BAA0C;ER2xElD;EQzxEM;;IAEE,6BAAsC;ER2xE9C;EQnxEM;IAAwB,2BAA2B;ERsxEzD;EQrxEM;;IAEE,+BAA+B;ERuxEvC;EQrxEM;;IAEE,iCAAiC;ERuxEzC;EQrxEM;;IAEE,kCAAkC;ERuxE1C;EQrxEM;;IAEE,gCAAgC;ERuxExC;EQtyEM;IAAwB,0BAA2B;ERyyEzD;EQxyEM;;IAEE,8BAA+B;ER0yEvC;EQxyEM;;IAEE,gCAAiC;ER0yEzC;EQxyEM;;IAEE,iCAAkC;ER0yE1C;EQxyEM;;IAEE,+BAAgC;ER0yExC;EQzzEM;IAAwB,wBAA2B;ER4zEzD;EQ3zEM;;IAEE,4BAA+B;ER6zEvC;EQ3zEM;;IAEE,8BAAiC;ER6zEzC;EQ3zEM;;IAEE,+BAAkC;ER6zE1C;EQ3zEM;;IAEE,6BAAgC;ER6zExC;EQ50EM;IAAwB,0BAA2B;ER+0EzD;EQ90EM;;IAEE,8BAA+B;ERg1EvC;EQ90EM;;IAEE,gCAAiC;ERg1EzC;EQ90EM;;IAEE,iCAAkC;ERg1E1C;EQ90EM;;IAEE,+BAAgC;ERg1ExC;EQ/1EM;IAAwB,wBAA2B;ERk2EzD;EQj2EM;;IAEE,4BAA+B;ERm2EvC;EQj2EM;;IAEE,8BAAiC;ERm2EzC;EQj2EM;;IAEE,+BAAkC;ERm2E1C;EQj2EM;;IAEE,6BAAgC;ERm2ExC;EQ71EE;IAAmB,uBAAuB;ERg2E5C;EQ/1EE;;IAEE,2BAA2B;ERi2E/B;EQ/1EE;;IAEE,6BAA6B;ERi2EjC;EQ/1EE;;IAEE,8BAA8B;ERi2ElC;EQ/1EE;;IAEE,4BAA4B;ERi2EhC;AACF;;AG32EI;EKlDI;IAAgC,oBAA4B;ERk6ElE;EQj6EM;;IAEE,wBAAoC;ERm6E5C;EQj6EM;;IAEE,0BAAwC;ERm6EhD;EQj6EM;;IAEE,2BAA0C;ERm6ElD;EQj6EM;;IAEE,yBAAsC;ERm6E9C;EQl7EM;IAAgC,0BAA4B;ERq7ElE;EQp7EM;;IAEE,8BAAoC;ERs7E5C;EQp7EM;;IAEE,gCAAwC;ERs7EhD;EQp7EM;;IAEE,iCAA0C;ERs7ElD;EQp7EM;;IAEE,+BAAsC;ERs7E9C;EQr8EM;IAAgC,yBAA4B;ERw8ElE;EQv8EM;;IAEE,6BAAoC;ERy8E5C;EQv8EM;;IAEE,+BAAwC;ERy8EhD;EQv8EM;;IAEE,gCAA0C;ERy8ElD;EQv8EM;;IAEE,8BAAsC;ERy8E9C;EQx9EM;IAAgC,uBAA4B;ER29ElE;EQ19EM;;IAEE,2BAAoC;ER49E5C;EQ19EM;;IAEE,6BAAwC;ER49EhD;EQ19EM;;IAEE,8BAA0C;ER49ElD;EQ19EM;;IAEE,4BAAsC;ER49E9C;EQ3+EM;IAAgC,yBAA4B;ER8+ElE;EQ7+EM;;IAEE,6BAAoC;ER++E5C;EQ7+EM;;IAEE,+BAAwC;ER++EhD;EQ7+EM;;IAEE,gCAA0C;ER++ElD;EQ7+EM;;IAEE,8BAAsC;ER++E9C;EQ9/EM;IAAgC,uBAA4B;ERigFlE;EQhgFM;;IAEE,2BAAoC;ERkgF5C;EQhgFM;;IAEE,6BAAwC;ERkgFhD;EQhgFM;;IAEE,8BAA0C;ERkgFlD;EQhgFM;;IAEE,4BAAsC;ERkgF9C;EQjhFM;IAAgC,qBAA4B;ERohFlE;EQnhFM;;IAEE,yBAAoC;ERqhF5C;EQnhFM;;IAEE,2BAAwC;ERqhFhD;EQnhFM;;IAEE,4BAA0C;ERqhFlD;EQnhFM;;IAEE,0BAAsC;ERqhF9C;EQpiFM;IAAgC,2BAA4B;ERuiFlE;EQtiFM;;IAEE,+BAAoC;ERwiF5C;EQtiFM;;IAEE,iCAAwC;ERwiFhD;EQtiFM;;IAEE,kCAA0C;ERwiFlD;EQtiFM;;IAEE,gCAAsC;ERwiF9C;EQvjFM;IAAgC,0BAA4B;ER0jFlE;EQzjFM;;IAEE,8BAAoC;ER2jF5C;EQzjFM;;IAEE,gCAAwC;ER2jFhD;EQzjFM;;IAEE,iCAA0C;ER2jFlD;EQzjFM;;IAEE,+BAAsC;ER2jF9C;EQ1kFM;IAAgC,wBAA4B;ER6kFlE;EQ5kFM;;IAEE,4BAAoC;ER8kF5C;EQ5kFM;;IAEE,8BAAwC;ER8kFhD;EQ5kFM;;IAEE,+BAA0C;ER8kFlD;EQ5kFM;;IAEE,6BAAsC;ER8kF9C;EQ7lFM;IAAgC,0BAA4B;ERgmFlE;EQ/lFM;;IAEE,8BAAoC;ERimF5C;EQ/lFM;;IAEE,gCAAwC;ERimFhD;EQ/lFM;;IAEE,iCAA0C;ERimFlD;EQ/lFM;;IAEE,+BAAsC;ERimF9C;EQhnFM;IAAgC,wBAA4B;ERmnFlE;EQlnFM;;IAEE,4BAAoC;ERonF5C;EQlnFM;;IAEE,8BAAwC;ERonFhD;EQlnFM;;IAEE,+BAA0C;ERonFlD;EQlnFM;;IAEE,6BAAsC;ERonF9C;EQ5mFM;IAAwB,2BAA2B;ER+mFzD;EQ9mFM;;IAEE,+BAA+B;ERgnFvC;EQ9mFM;;IAEE,iCAAiC;ERgnFzC;EQ9mFM;;IAEE,kCAAkC;ERgnF1C;EQ9mFM;;IAEE,gCAAgC;ERgnFxC;EQ/nFM;IAAwB,0BAA2B;ERkoFzD;EQjoFM;;IAEE,8BAA+B;ERmoFvC;EQjoFM;;IAEE,gCAAiC;ERmoFzC;EQjoFM;;IAEE,iCAAkC;ERmoF1C;EQjoFM;;IAEE,+BAAgC;ERmoFxC;EQlpFM;IAAwB,wBAA2B;ERqpFzD;EQppFM;;IAEE,4BAA+B;ERspFvC;EQppFM;;IAEE,8BAAiC;ERspFzC;EQppFM;;IAEE,+BAAkC;ERspF1C;EQppFM;;IAEE,6BAAgC;ERspFxC;EQrqFM;IAAwB,0BAA2B;ERwqFzD;EQvqFM;;IAEE,8BAA+B;ERyqFvC;EQvqFM;;IAEE,gCAAiC;ERyqFzC;EQvqFM;;IAEE,iCAAkC;ERyqF1C;EQvqFM;;IAEE,+BAAgC;ERyqFxC;EQxrFM;IAAwB,wBAA2B;ER2rFzD;EQ1rFM;;IAEE,4BAA+B;ER4rFvC;EQ1rFM;;IAEE,8BAAiC;ER4rFzC;EQ1rFM;;IAEE,+BAAkC;ER4rF1C;EQ1rFM;;IAEE,6BAAgC;ER4rFxC;EQtrFE;IAAmB,uBAAuB;ERyrF5C;EQxrFE;;IAEE,2BAA2B;ER0rF/B;EQxrFE;;IAEE,6BAA6B;ER0rFjC;EQxrFE;;IAEE,8BAA8B;ER0rFlC;EQxrFE;;IAEE,4BAA4B;ER0rFhC;AACF;;AGpsFI;EKlDI;IAAgC,oBAA4B;ER2vFlE;EQ1vFM;;IAEE,wBAAoC;ER4vF5C;EQ1vFM;;IAEE,0BAAwC;ER4vFhD;EQ1vFM;;IAEE,2BAA0C;ER4vFlD;EQ1vFM;;IAEE,yBAAsC;ER4vF9C;EQ3wFM;IAAgC,0BAA4B;ER8wFlE;EQ7wFM;;IAEE,8BAAoC;ER+wF5C;EQ7wFM;;IAEE,gCAAwC;ER+wFhD;EQ7wFM;;IAEE,iCAA0C;ER+wFlD;EQ7wFM;;IAEE,+BAAsC;ER+wF9C;EQ9xFM;IAAgC,yBAA4B;ERiyFlE;EQhyFM;;IAEE,6BAAoC;ERkyF5C;EQhyFM;;IAEE,+BAAwC;ERkyFhD;EQhyFM;;IAEE,gCAA0C;ERkyFlD;EQhyFM;;IAEE,8BAAsC;ERkyF9C;EQjzFM;IAAgC,uBAA4B;ERozFlE;EQnzFM;;IAEE,2BAAoC;ERqzF5C;EQnzFM;;IAEE,6BAAwC;ERqzFhD;EQnzFM;;IAEE,8BAA0C;ERqzFlD;EQnzFM;;IAEE,4BAAsC;ERqzF9C;EQp0FM;IAAgC,yBAA4B;ERu0FlE;EQt0FM;;IAEE,6BAAoC;ERw0F5C;EQt0FM;;IAEE,+BAAwC;ERw0FhD;EQt0FM;;IAEE,gCAA0C;ERw0FlD;EQt0FM;;IAEE,8BAAsC;ERw0F9C;EQv1FM;IAAgC,uBAA4B;ER01FlE;EQz1FM;;IAEE,2BAAoC;ER21F5C;EQz1FM;;IAEE,6BAAwC;ER21FhD;EQz1FM;;IAEE,8BAA0C;ER21FlD;EQz1FM;;IAEE,4BAAsC;ER21F9C;EQ12FM;IAAgC,qBAA4B;ER62FlE;EQ52FM;;IAEE,yBAAoC;ER82F5C;EQ52FM;;IAEE,2BAAwC;ER82FhD;EQ52FM;;IAEE,4BAA0C;ER82FlD;EQ52FM;;IAEE,0BAAsC;ER82F9C;EQ73FM;IAAgC,2BAA4B;ERg4FlE;EQ/3FM;;IAEE,+BAAoC;ERi4F5C;EQ/3FM;;IAEE,iCAAwC;ERi4FhD;EQ/3FM;;IAEE,kCAA0C;ERi4FlD;EQ/3FM;;IAEE,gCAAsC;ERi4F9C;EQh5FM;IAAgC,0BAA4B;ERm5FlE;EQl5FM;;IAEE,8BAAoC;ERo5F5C;EQl5FM;;IAEE,gCAAwC;ERo5FhD;EQl5FM;;IAEE,iCAA0C;ERo5FlD;EQl5FM;;IAEE,+BAAsC;ERo5F9C;EQn6FM;IAAgC,wBAA4B;ERs6FlE;EQr6FM;;IAEE,4BAAoC;ERu6F5C;EQr6FM;;IAEE,8BAAwC;ERu6FhD;EQr6FM;;IAEE,+BAA0C;ERu6FlD;EQr6FM;;IAEE,6BAAsC;ERu6F9C;EQt7FM;IAAgC,0BAA4B;ERy7FlE;EQx7FM;;IAEE,8BAAoC;ER07F5C;EQx7FM;;IAEE,gCAAwC;ER07FhD;EQx7FM;;IAEE,iCAA0C;ER07FlD;EQx7FM;;IAEE,+BAAsC;ER07F9C;EQz8FM;IAAgC,wBAA4B;ER48FlE;EQ38FM;;IAEE,4BAAoC;ER68F5C;EQ38FM;;IAEE,8BAAwC;ER68FhD;EQ38FM;;IAEE,+BAA0C;ER68FlD;EQ38FM;;IAEE,6BAAsC;ER68F9C;EQr8FM;IAAwB,2BAA2B;ERw8FzD;EQv8FM;;IAEE,+BAA+B;ERy8FvC;EQv8FM;;IAEE,iCAAiC;ERy8FzC;EQv8FM;;IAEE,kCAAkC;ERy8F1C;EQv8FM;;IAEE,gCAAgC;ERy8FxC;EQx9FM;IAAwB,0BAA2B;ER29FzD;EQ19FM;;IAEE,8BAA+B;ER49FvC;EQ19FM;;IAEE,gCAAiC;ER49FzC;EQ19FM;;IAEE,iCAAkC;ER49F1C;EQ19FM;;IAEE,+BAAgC;ER49FxC;EQ3+FM;IAAwB,wBAA2B;ER8+FzD;EQ7+FM;;IAEE,4BAA+B;ER++FvC;EQ7+FM;;IAEE,8BAAiC;ER++FzC;EQ7+FM;;IAEE,+BAAkC;ER++F1C;EQ7+FM;;IAEE,6BAAgC;ER++FxC;EQ9/FM;IAAwB,0BAA2B;ERigGzD;EQhgGM;;IAEE,8BAA+B;ERkgGvC;EQhgGM;;IAEE,gCAAiC;ERkgGzC;EQhgGM;;IAEE,iCAAkC;ERkgG1C;EQhgGM;;IAEE,+BAAgC;ERkgGxC;EQjhGM;IAAwB,wBAA2B;ERohGzD;EQnhGM;;IAEE,4BAA+B;ERqhGvC;EQnhGM;;IAEE,8BAAiC;ERqhGzC;EQnhGM;;IAEE,+BAAkC;ERqhG1C;EQnhGM;;IAEE,6BAAgC;ERqhGxC;EQ/gGE;IAAmB,uBAAuB;ERkhG5C;EQjhGE;;IAEE,2BAA2B;ERmhG/B;EQjhGE;;IAEE,6BAA6B;ERmhGjC;EQjhGE;;IAEE,8BAA8B;ERmhGlC;EQjhGE;;IAEE,4BAA4B;ERmhGhC;AACF;;AG7hGI;EKlDI;IAAgC,oBAA4B;ERolGlE;EQnlGM;;IAEE,wBAAoC;ERqlG5C;EQnlGM;;IAEE,0BAAwC;ERqlGhD;EQnlGM;;IAEE,2BAA0C;ERqlGlD;EQnlGM;;IAEE,yBAAsC;ERqlG9C;EQpmGM;IAAgC,0BAA4B;ERumGlE;EQtmGM;;IAEE,8BAAoC;ERwmG5C;EQtmGM;;IAEE,gCAAwC;ERwmGhD;EQtmGM;;IAEE,iCAA0C;ERwmGlD;EQtmGM;;IAEE,+BAAsC;ERwmG9C;EQvnGM;IAAgC,yBAA4B;ER0nGlE;EQznGM;;IAEE,6BAAoC;ER2nG5C;EQznGM;;IAEE,+BAAwC;ER2nGhD;EQznGM;;IAEE,gCAA0C;ER2nGlD;EQznGM;;IAEE,8BAAsC;ER2nG9C;EQ1oGM;IAAgC,uBAA4B;ER6oGlE;EQ5oGM;;IAEE,2BAAoC;ER8oG5C;EQ5oGM;;IAEE,6BAAwC;ER8oGhD;EQ5oGM;;IAEE,8BAA0C;ER8oGlD;EQ5oGM;;IAEE,4BAAsC;ER8oG9C;EQ7pGM;IAAgC,yBAA4B;ERgqGlE;EQ/pGM;;IAEE,6BAAoC;ERiqG5C;EQ/pGM;;IAEE,+BAAwC;ERiqGhD;EQ/pGM;;IAEE,gCAA0C;ERiqGlD;EQ/pGM;;IAEE,8BAAsC;ERiqG9C;EQhrGM;IAAgC,uBAA4B;ERmrGlE;EQlrGM;;IAEE,2BAAoC;ERorG5C;EQlrGM;;IAEE,6BAAwC;ERorGhD;EQlrGM;;IAEE,8BAA0C;ERorGlD;EQlrGM;;IAEE,4BAAsC;ERorG9C;EQnsGM;IAAgC,qBAA4B;ERssGlE;EQrsGM;;IAEE,yBAAoC;ERusG5C;EQrsGM;;IAEE,2BAAwC;ERusGhD;EQrsGM;;IAEE,4BAA0C;ERusGlD;EQrsGM;;IAEE,0BAAsC;ERusG9C;EQttGM;IAAgC,2BAA4B;ERytGlE;EQxtGM;;IAEE,+BAAoC;ER0tG5C;EQxtGM;;IAEE,iCAAwC;ER0tGhD;EQxtGM;;IAEE,kCAA0C;ER0tGlD;EQxtGM;;IAEE,gCAAsC;ER0tG9C;EQzuGM;IAAgC,0BAA4B;ER4uGlE;EQ3uGM;;IAEE,8BAAoC;ER6uG5C;EQ3uGM;;IAEE,gCAAwC;ER6uGhD;EQ3uGM;;IAEE,iCAA0C;ER6uGlD;EQ3uGM;;IAEE,+BAAsC;ER6uG9C;EQ5vGM;IAAgC,wBAA4B;ER+vGlE;EQ9vGM;;IAEE,4BAAoC;ERgwG5C;EQ9vGM;;IAEE,8BAAwC;ERgwGhD;EQ9vGM;;IAEE,+BAA0C;ERgwGlD;EQ9vGM;;IAEE,6BAAsC;ERgwG9C;EQ/wGM;IAAgC,0BAA4B;ERkxGlE;EQjxGM;;IAEE,8BAAoC;ERmxG5C;EQjxGM;;IAEE,gCAAwC;ERmxGhD;EQjxGM;;IAEE,iCAA0C;ERmxGlD;EQjxGM;;IAEE,+BAAsC;ERmxG9C;EQlyGM;IAAgC,wBAA4B;ERqyGlE;EQpyGM;;IAEE,4BAAoC;ERsyG5C;EQpyGM;;IAEE,8BAAwC;ERsyGhD;EQpyGM;;IAEE,+BAA0C;ERsyGlD;EQpyGM;;IAEE,6BAAsC;ERsyG9C;EQ9xGM;IAAwB,2BAA2B;ERiyGzD;EQhyGM;;IAEE,+BAA+B;ERkyGvC;EQhyGM;;IAEE,iCAAiC;ERkyGzC;EQhyGM;;IAEE,kCAAkC;ERkyG1C;EQhyGM;;IAEE,gCAAgC;ERkyGxC;EQjzGM;IAAwB,0BAA2B;ERozGzD;EQnzGM;;IAEE,8BAA+B;ERqzGvC;EQnzGM;;IAEE,gCAAiC;ERqzGzC;EQnzGM;;IAEE,iCAAkC;ERqzG1C;EQnzGM;;IAEE,+BAAgC;ERqzGxC;EQp0GM;IAAwB,wBAA2B;ERu0GzD;EQt0GM;;IAEE,4BAA+B;ERw0GvC;EQt0GM;;IAEE,8BAAiC;ERw0GzC;EQt0GM;;IAEE,+BAAkC;ERw0G1C;EQt0GM;;IAEE,6BAAgC;ERw0GxC;EQv1GM;IAAwB,0BAA2B;ER01GzD;EQz1GM;;IAEE,8BAA+B;ER21GvC;EQz1GM;;IAEE,gCAAiC;ER21GzC;EQz1GM;;IAEE,iCAAkC;ER21G1C;EQz1GM;;IAEE,+BAAgC;ER21GxC;EQ12GM;IAAwB,wBAA2B;ER62GzD;EQ52GM;;IAEE,4BAA+B;ER82GvC;EQ52GM;;IAEE,8BAAiC;ER82GzC;EQ52GM;;IAEE,+BAAkC;ER82G1C;EQ52GM;;IAEE,6BAAgC;ER82GxC;EQx2GE;IAAmB,uBAAuB;ER22G5C;EQ12GE;;IAEE,2BAA2B;ER42G/B;EQ12GE;;IAEE,6BAA6B;ER42GjC;EQ12GE;;IAEE,8BAA8B;ER42GlC;EQ12GE;;IAEE,4BAA4B;ER42GhC;AACF","file":"bootstrap-grid.css","sourcesContent":["/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container,\n.container-fluid,\n.container-sm,\n.container-md,\n.container-lg,\n.container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n @include deprecate(\"The `make-container-max-widths` mixin\", \"v4.5.2\", \"v5\");\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n\n$grays: () !default;\n$grays: map-merge(\n (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n ),\n $grays\n);\n\n$blue: #007bff !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #e83e8c !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #28a745 !default;\n$teal: #20c997 !default;\n$cyan: #17a2b8 !default;\n\n$colors: () !default;\n$colors: map-merge(\n (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n ),\n $colors\n);\n\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-800 !default;\n\n$theme-colors: () !default;\n$theme-colors: map-merge(\n (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n ),\n $theme-colors\n);\n\n// Set a specific jump point for requesting color jumps\n$theme-color-interval: 8% !default;\n\n// The yiq lightness value that determines when the lightness of color changes from \"dark\" to \"light\". Acceptable values are between 0 and 255.\n$yiq-contrasted-threshold: 150 !default;\n\n// Customize the light and dark text colors for use in our YIQ color contrast function.\n$yiq-text-dark: $gray-900 !default;\n$yiq-text-light: $white !default;\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\", \"%3c\"),\n (\">\", \"%3e\"),\n (\"#\", \"%23\"),\n (\"(\", \"%28\"),\n (\")\", \"%29\"),\n) !default;\n\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-prefers-reduced-motion-media-query: true !default;\n$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS\n$enable-grid-classes: true !default;\n$enable-pointer-cursor-for-buttons: true !default;\n$enable-print-styles: true !default;\n$enable-responsive-font-sizes: false !default;\n$enable-validation-icons: true !default;\n$enable-deprecation-messages: true !default;\n\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n$spacer: 1rem !default;\n$spacers: () !default;\n$spacers: map-merge(\n (\n 0: 0,\n 1: ($spacer * .25),\n 2: ($spacer * .5),\n 3: $spacer,\n 4: ($spacer * 1.5),\n 5: ($spacer * 3)\n ),\n $spacers\n);\n\n// This variable affects the `.h-*` and `.w-*` classes.\n$sizes: () !default;\n$sizes: map-merge(\n (\n 25: 25%,\n 50: 50%,\n 75: 75%,\n 100: 100%,\n auto: auto\n ),\n $sizes\n);\n\n\n// Body\n//\n// Settings for the `` element.\n\n$body-bg: $white !default;\n$body-color: $gray-900 !default;\n\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: theme-color(\"primary\") !default;\n$link-decoration: none !default;\n$link-hover-color: darken($link-color, 15%) !default;\n$link-hover-decoration: underline !default;\n// Darken percentage for links with `.text-*` class (e.g. `.text-success`)\n$emphasized-link-hover-darken-percentage: 15% !default;\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px\n) !default;\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px\n) !default;\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 30px !default;\n$grid-row-columns: 6 !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n$line-height-lg: 1.5 !default;\n$line-height-sm: 1.5 !default;\n\n$border-width: 1px !default;\n$border-color: $gray-300 !default;\n\n$border-radius: .25rem !default;\n$border-radius-lg: .3rem !default;\n$border-radius-sm: .2rem !default;\n\n$rounded-pill: 50rem !default;\n\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n\n$component-active-color: $white !default;\n$component-active-bg: theme-color(\"primary\") !default;\n\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n$transition-collapse: height .35s ease !default;\n\n$embed-responsive-aspect-ratios: () !default;\n$embed-responsive-aspect-ratios: join(\n (\n (21 9),\n (16 9),\n (4 3),\n (1 1),\n ),\n $embed-responsive-aspect-ratios\n);\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n$font-family-base: $font-family-sans-serif !default;\n// stylelint-enable value-keyword-case\n\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-lg: $font-size-base * 1.25 !default;\n$font-size-sm: $font-size-base * .875 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n$line-height-base: 1.5 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n\n$headings-margin-bottom: $spacer / 2 !default;\n$headings-font-family: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: null !default;\n\n$display1-size: 6rem !default;\n$display2-size: 5.5rem !default;\n$display3-size: 4.5rem !default;\n$display4-size: 3.5rem !default;\n\n$display1-weight: 300 !default;\n$display2-weight: 300 !default;\n$display3-weight: 300 !default;\n$display4-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: 80% !default;\n\n$text-muted: $gray-600 !default;\n\n$blockquote-small-color: $gray-600 !default;\n$blockquote-small-font-size: $small-font-size !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n\n$hr-border-color: rgba($black, .1) !default;\n$hr-border-width: $border-width !default;\n\n$mark-padding: .2em !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;\n$nested-kbd-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-bg: #fcf8e3 !default;\n\n$hr-margin-y: $spacer !default;\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n$table-cell-padding: .75rem !default;\n$table-cell-padding-sm: .3rem !default;\n\n$table-color: $body-color !default;\n$table-bg: null !default;\n$table-accent-bg: rgba($black, .05) !default;\n$table-hover-color: $table-color !default;\n$table-hover-bg: rgba($black, .075) !default;\n$table-active-bg: $table-hover-bg !default;\n\n$table-border-width: $border-width !default;\n$table-border-color: $border-color !default;\n\n$table-head-bg: $gray-200 !default;\n$table-head-color: $gray-700 !default;\n$table-th-font-weight: null !default;\n\n$table-dark-color: $white !default;\n$table-dark-bg: $gray-800 !default;\n$table-dark-accent-bg: rgba($white, .05) !default;\n$table-dark-hover-color: $table-dark-color !default;\n$table-dark-hover-bg: rgba($white, .075) !default;\n$table-dark-border-color: lighten($table-dark-bg, 7.5%) !default;\n\n$table-striped-order: odd !default;\n\n$table-caption-color: $text-muted !default;\n\n$table-bg-level: -9 !default;\n$table-border-level: -6 !default;\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: .2rem !default;\n$input-btn-focus-color: rgba($component-active-bg, .25) !default;\n$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n$input-btn-line-height-sm: $line-height-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n$input-btn-line-height-lg: $line-height-lg !default;\n\n$input-btn-border-width: $border-width !default;\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n$btn-line-height-sm: $input-btn-line-height-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n$btn-line-height-lg: $input-btn-line-height-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-disabled-color: $gray-600 !default;\n\n$btn-block-spacing-y: .5rem !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: $border-radius !default;\n$btn-border-radius-lg: $border-radius-lg !default;\n$btn-border-radius-sm: $border-radius-sm !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n\n// Forms\n\n$label-margin-bottom: .5rem !default;\n\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n$input-line-height-sm: $input-btn-line-height-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n$input-line-height-lg: $input-btn-line-height-lg !default;\n\n$input-bg: $white !default;\n$input-disabled-bg: $gray-200 !default;\n\n$input-color: $gray-700 !default;\n$input-border-color: $gray-400 !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;\n\n$input-border-radius: $border-radius !default;\n$input-border-radius-lg: $border-radius-lg !default;\n$input-border-radius-sm: $border-radius-sm !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: lighten($component-active-bg, 25%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: $gray-600 !default;\n$input-plaintext-color: $body-color !default;\n\n$input-height-border: $input-border-width * 2 !default;\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y / 2) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height-lg * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-text-margin-top: .25rem !default;\n\n$form-check-input-gutter: 1.25rem !default;\n$form-check-input-margin-y: .3rem !default;\n$form-check-input-margin-x: .25rem !default;\n\n$form-check-inline-margin-x: .75rem !default;\n$form-check-inline-input-margin-x: .3125rem !default;\n\n$form-grid-gutter-width: 10px !default;\n$form-group-margin-bottom: 1rem !default;\n\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: $gray-200 !default;\n$input-group-addon-border-color: $input-border-color !default;\n\n$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$custom-control-gutter: .5rem !default;\n$custom-control-spacer-x: 1rem !default;\n$custom-control-cursor: null !default;\n\n$custom-control-indicator-size: 1rem !default;\n$custom-control-indicator-bg: $input-bg !default;\n\n$custom-control-indicator-bg-size: 50% 50% !default;\n$custom-control-indicator-box-shadow: $input-box-shadow !default;\n$custom-control-indicator-border-color: $gray-500 !default;\n$custom-control-indicator-border-width: $input-border-width !default;\n\n$custom-control-label-color: null !default;\n\n$custom-control-indicator-disabled-bg: $input-disabled-bg !default;\n$custom-control-label-disabled-color: $gray-600 !default;\n\n$custom-control-indicator-checked-color: $component-active-color !default;\n$custom-control-indicator-checked-bg: $component-active-bg !default;\n$custom-control-indicator-checked-disabled-bg: rgba(theme-color(\"primary\"), .5) !default;\n$custom-control-indicator-checked-box-shadow: null !default;\n$custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg !default;\n\n$custom-control-indicator-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-control-indicator-focus-border-color: $input-focus-border-color !default;\n\n$custom-control-indicator-active-color: $component-active-color !default;\n$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-control-indicator-active-box-shadow: null !default;\n$custom-control-indicator-active-border-color: $custom-control-indicator-active-bg !default;\n\n$custom-checkbox-indicator-border-radius: $border-radius !default;\n$custom-checkbox-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;\n$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;\n$custom-checkbox-indicator-icon-indeterminate: url(\"data:image/svg+xml,\") !default;\n$custom-checkbox-indicator-indeterminate-box-shadow: null !default;\n$custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg !default;\n\n$custom-radio-indicator-border-radius: 50% !default;\n$custom-radio-indicator-icon-checked: url(\"data:image/svg+xml,\") !default;\n\n$custom-switch-width: $custom-control-indicator-size * 1.75 !default;\n$custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 !default;\n$custom-switch-indicator-size: subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;\n\n$custom-select-padding-y: $input-padding-y !default;\n$custom-select-padding-x: $input-padding-x !default;\n$custom-select-font-family: $input-font-family !default;\n$custom-select-font-size: $input-font-size !default;\n$custom-select-height: $input-height !default;\n$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator\n$custom-select-font-weight: $input-font-weight !default;\n$custom-select-line-height: $input-line-height !default;\n$custom-select-color: $input-color !default;\n$custom-select-disabled-color: $gray-600 !default;\n$custom-select-bg: $input-bg !default;\n$custom-select-disabled-bg: $gray-200 !default;\n$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions\n$custom-select-indicator-color: $gray-800 !default;\n$custom-select-indicator: url(\"data:image/svg+xml,\") !default;\n$custom-select-background: escape-svg($custom-select-indicator) no-repeat right $custom-select-padding-x center / $custom-select-bg-size !default; // Used so we can have multiple background elements (e.g., arrow and feedback icon)\n\n$custom-select-feedback-icon-padding-right: add(1em * .75, (2 * $custom-select-padding-y * .75) + $custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) !default;\n$custom-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$custom-select-border-width: $input-border-width !default;\n$custom-select-border-color: $input-border-color !default;\n$custom-select-border-radius: $border-radius !default;\n$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;\n\n$custom-select-focus-border-color: $input-focus-border-color !default;\n$custom-select-focus-width: $input-focus-width !default;\n$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width $input-btn-focus-color !default;\n\n$custom-select-padding-y-sm: $input-padding-y-sm !default;\n$custom-select-padding-x-sm: $input-padding-x-sm !default;\n$custom-select-font-size-sm: $input-font-size-sm !default;\n$custom-select-height-sm: $input-height-sm !default;\n\n$custom-select-padding-y-lg: $input-padding-y-lg !default;\n$custom-select-padding-x-lg: $input-padding-x-lg !default;\n$custom-select-font-size-lg: $input-font-size-lg !default;\n$custom-select-height-lg: $input-height-lg !default;\n\n$custom-range-track-width: 100% !default;\n$custom-range-track-height: .5rem !default;\n$custom-range-track-cursor: pointer !default;\n$custom-range-track-bg: $gray-300 !default;\n$custom-range-track-border-radius: 1rem !default;\n$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;\n\n$custom-range-thumb-width: 1rem !default;\n$custom-range-thumb-height: $custom-range-thumb-width !default;\n$custom-range-thumb-bg: $component-active-bg !default;\n$custom-range-thumb-border: 0 !default;\n$custom-range-thumb-border-radius: 1rem !default;\n$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$custom-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in IE/Edge\n$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;\n$custom-range-thumb-disabled-bg: $gray-500 !default;\n\n$custom-file-height: $input-height !default;\n$custom-file-height-inner: $input-height-inner !default;\n$custom-file-focus-border-color: $input-focus-border-color !default;\n$custom-file-focus-box-shadow: $input-focus-box-shadow !default;\n$custom-file-disabled-bg: $input-disabled-bg !default;\n\n$custom-file-padding-y: $input-padding-y !default;\n$custom-file-padding-x: $input-padding-x !default;\n$custom-file-line-height: $input-line-height !default;\n$custom-file-font-family: $input-font-family !default;\n$custom-file-font-weight: $input-font-weight !default;\n$custom-file-color: $input-color !default;\n$custom-file-bg: $input-bg !default;\n$custom-file-border-width: $input-border-width !default;\n$custom-file-border-color: $input-border-color !default;\n$custom-file-border-radius: $input-border-radius !default;\n$custom-file-box-shadow: $input-box-shadow !default;\n$custom-file-button-color: $custom-file-color !default;\n$custom-file-button-bg: $input-group-addon-bg !default;\n$custom-file-text: (\n en: \"Browse\"\n) !default;\n\n\n// Form validation\n\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $small-font-size !default;\n$form-feedback-valid-color: theme-color(\"success\") !default;\n$form-feedback-invalid-color: theme-color(\"danger\") !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n\n$form-validation-states: () !default;\n$form-validation-states: map-merge(\n (\n \"valid\": (\n \"color\": $form-feedback-valid-color,\n \"icon\": $form-feedback-icon-valid\n ),\n \"invalid\": (\n \"color\": $form-feedback-invalid-color,\n \"icon\": $form-feedback-icon-invalid\n ),\n ),\n $form-validation-states\n);\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-modal-backdrop: 1040 !default;\n$zindex-modal: 1050 !default;\n$zindex-popover: 1060 !default;\n$zindex-tooltip: 1070 !default;\n\n\n// Navs\n\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-disabled-color: $gray-600 !default;\n\n$nav-tabs-border-color: $gray-300 !default;\n$nav-tabs-border-width: $border-width !default;\n$nav-tabs-border-radius: $border-radius !default;\n$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: $gray-700 !default;\n$nav-tabs-link-active-bg: $body-bg !default;\n$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: $border-radius !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-divider-color: $gray-200 !default;\n$nav-divider-margin-y: $spacer / 2 !default;\n\n\n// Navbar\n\n$navbar-padding-y: $spacer / 2 !default;\n$navbar-padding-x: $spacer !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n\n$navbar-dark-color: rgba($white, .5) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n\n$navbar-light-color: rgba($black, .5) !default;\n$navbar-light-hover-color: rgba($black, .7) !default;\n$navbar-light-active-color: rgba($black, .9) !default;\n$navbar-light-disabled-color: rgba($black, .3) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba($black, .1) !default;\n\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-x: 0 !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: $body-color !default;\n$dropdown-bg: $white !default;\n$dropdown-border-color: rgba($black, .15) !default;\n$dropdown-border-radius: $border-radius !default;\n$dropdown-border-width: $border-width !default;\n$dropdown-inner-border-radius: subtract($dropdown-border-radius, $dropdown-border-width) !default;\n$dropdown-divider-bg: $gray-200 !default;\n$dropdown-divider-margin-y: $nav-divider-margin-y !default;\n$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;\n\n$dropdown-link-color: $gray-900 !default;\n$dropdown-link-hover-color: darken($gray-900, 5%) !default;\n$dropdown-link-hover-bg: $gray-100 !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: $gray-600 !default;\n\n$dropdown-item-padding-y: .25rem !default;\n$dropdown-item-padding-x: 1.5rem !default;\n\n$dropdown-header-color: $gray-600 !default;\n$dropdown-header-padding: $dropdown-padding-y $dropdown-item-padding-x !default;\n\n\n// Pagination\n\n$pagination-padding-y: .5rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n$pagination-line-height: 1.25 !default;\n\n$pagination-color: $link-color !default;\n$pagination-bg: $white !default;\n$pagination-border-width: $border-width !default;\n$pagination-border-color: $gray-300 !default;\n\n$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: $link-hover-color !default;\n$pagination-hover-bg: $gray-200 !default;\n$pagination-hover-border-color: $gray-300 !default;\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $pagination-active-bg !default;\n\n$pagination-disabled-color: $gray-600 !default;\n$pagination-disabled-bg: $white !default;\n$pagination-disabled-border-color: $gray-300 !default;\n\n\n// Jumbotron\n\n$jumbotron-padding: 2rem !default;\n$jumbotron-color: null !default;\n$jumbotron-bg: $gray-200 !default;\n\n\n// Cards\n\n$card-spacer-y: .75rem !default;\n$card-spacer-x: 1.25rem !default;\n$card-border-width: $border-width !default;\n$card-border-radius: $border-radius !default;\n$card-border-color: rgba($black, .125) !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-bg: rgba($black, .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: $white !default;\n\n$card-img-overlay-padding: 1.25rem !default;\n\n$card-group-margin: $grid-gutter-width / 2 !default;\n$card-deck-margin: $card-group-margin !default;\n\n$card-columns-count: 3 !default;\n$card-columns-gap: 1.25rem !default;\n$card-columns-margin: $card-spacer-y !default;\n\n\n// Tooltips\n\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: $white !default;\n$tooltip-bg: $black !default;\n$tooltip-border-radius: $border-radius !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: .25rem !default;\n$tooltip-padding-x: .5rem !default;\n$tooltip-margin: 0 !default;\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n$tooltip-arrow-color: $tooltip-bg !default;\n\n// Form tooltips must come after regular tooltips\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: $line-height-base !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n\n\n// Popovers\n\n$popover-font-size: $font-size-sm !default;\n$popover-bg: $white !default;\n$popover-max-width: 276px !default;\n$popover-border-width: $border-width !default;\n$popover-border-color: rgba($black, .2) !default;\n$popover-border-radius: $border-radius-lg !default;\n$popover-inner-border-radius: subtract($popover-border-radius, $popover-border-width) !default;\n$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;\n\n$popover-header-bg: darken($popover-bg, 3%) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: .75rem !default;\n\n$popover-body-color: $body-color !default;\n$popover-body-padding-y: $popover-header-padding-y !default;\n$popover-body-padding-x: $popover-header-padding-x !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n$popover-arrow-color: $popover-bg !default;\n\n$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;\n\n\n// Toasts\n\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .25rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba($white, .85) !default;\n$toast-border-width: 1px !default;\n$toast-border-color: rgba(0, 0, 0, .1) !default;\n$toast-border-radius: .25rem !default;\n$toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;\n\n$toast-header-color: $gray-600 !default;\n$toast-header-background-color: rgba($white, .85) !default;\n$toast-header-border-color: rgba(0, 0, 0, .05) !default;\n\n\n// Badges\n\n$badge-font-size: 75% !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-padding-y: .25em !default;\n$badge-padding-x: .4em !default;\n$badge-border-radius: $border-radius !default;\n\n$badge-transition: $btn-transition !default;\n$badge-focus-width: $input-btn-focus-width !default;\n\n$badge-pill-padding-x: .6em !default;\n// Use a higher than normal value to ensure completely rounded edges when\n// customizing padding or font-size on labels.\n$badge-pill-border-radius: 10rem !default;\n\n\n// Modals\n\n// Padding applied to the modal body\n$modal-inner-padding: 1rem !default;\n\n// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: null !default;\n$modal-content-bg: $white !default;\n$modal-content-border-color: rgba($black, .2) !default;\n$modal-content-border-width: $border-width !default;\n$modal-content-border-radius: $border-radius-lg !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;\n$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n$modal-header-border-color: $border-color !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n$modal-header-padding-y: 1rem !default;\n$modal-header-padding-x: 1rem !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-xl: 1140px !default;\n$modal-lg: 800px !default;\n$modal-md: 500px !default;\n$modal-sm: 300px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n$alert-padding-y: .75rem !default;\n$alert-padding-x: 1.25rem !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: $border-radius !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: $border-width !default;\n\n$alert-bg-level: -10 !default;\n$alert-border-level: -9 !default;\n$alert-color-level: 6 !default;\n\n\n// Progress bars\n\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: $gray-200 !default;\n$progress-border-radius: $border-radius !default;\n$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: theme-color(\"primary\") !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n\n\n// List group\n\n$list-group-color: null !default;\n$list-group-bg: $white !default;\n$list-group-border-color: rgba($black, .125) !default;\n$list-group-border-width: $border-width !default;\n$list-group-border-radius: $border-radius !default;\n\n$list-group-item-padding-y: .75rem !default;\n$list-group-item-padding-x: 1.25rem !default;\n\n$list-group-hover-bg: $gray-100 !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: $gray-600 !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: $gray-700 !default;\n$list-group-action-hover-color: $list-group-action-color !default;\n\n$list-group-action-active-color: $body-color !default;\n$list-group-action-active-bg: $gray-200 !default;\n\n\n// Image thumbnails\n\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: $body-bg !default;\n$thumbnail-border-width: $border-width !default;\n$thumbnail-border-color: $gray-300 !default;\n$thumbnail-border-radius: $border-radius !default;\n$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;\n\n\n// Figures\n\n$figure-caption-font-size: 90% !default;\n$figure-caption-color: $gray-600 !default;\n\n\n// Breadcrumbs\n\n$breadcrumb-font-size: null !default;\n\n$breadcrumb-padding-y: .75rem !default;\n$breadcrumb-padding-x: 1rem !default;\n$breadcrumb-item-padding: .5rem !default;\n\n$breadcrumb-margin-bottom: 1rem !default;\n\n$breadcrumb-bg: $gray-200 !default;\n$breadcrumb-divider-color: $gray-600 !default;\n$breadcrumb-active-color: $gray-600 !default;\n$breadcrumb-divider: quote(\"/\") !default;\n\n$breadcrumb-border-radius: $border-radius !default;\n\n\n// Carousel\n\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n\n$carousel-control-icon-width: 20px !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n\n\n// Spinners\n\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-border-width: .25em !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n\n\n// Close\n\n$close-font-size: $font-size-base * 1.5 !default;\n$close-font-weight: $font-weight-bold !default;\n$close-color: $black !default;\n$close-text-shadow: 0 1px 0 $white !default;\n\n\n// Code\n\n$code-font-size: 87.5% !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .2rem !default;\n$kbd-padding-x: .4rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: $white !default;\n$kbd-bg: $gray-900 !default;\n\n$pre-color: $gray-900 !default;\n$pre-scrollable-max-height: 340px !default;\n\n\n// Utilities\n\n$displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;\n$overflows: auto, hidden !default;\n$positions: static, relative, absolute, fixed, sticky !default;\n$user-selects: all, auto, none !default;\n\n\n// Printing\n\n$print-page-size: a3 !default;\n$print-body-min-width: map-get($grid-breakpoints, \"lg\") !default;\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @if $columns > 0 {\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n }\n\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n @if $columns > 0 {\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css new file mode 100644 index 00000000..d323f93f --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid v4.5.3 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map new file mode 100644 index 00000000..9c96ff30 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-grid.scss","dist/css/bootstrap-grid.css","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/utilities/_display.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_spacing.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n@import \"functions\";\n@import \"variables\";\n\n@import \"mixins/breakpoints\";\n@import \"mixins/grid-framework\";\n@import \"mixins/grid\";\n\n@import \"grid\";\n@import \"utilities/display\";\n@import \"utilities/flex\";\n@import \"utilities/spacing\";\n","/*!\n * Bootstrap Grid v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nhtml {\n box-sizing: border-box;\n -ms-overflow-style: scrollbar;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n.container,\n.container-fluid,\n.container-sm,\n.container-md,\n.container-lg,\n.container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n @include deprecate(\"The `make-container-max-widths` mixin\", \"v4.5.2\", \"v5\");\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @if $columns > 0 {\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n }\n\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n @if $columns > 0 {\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n"]} \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css new file mode 100644 index 00000000..4c642187 --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css @@ -0,0 +1,326 @@ +/*! + * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 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; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus:not(:focus-visible) { + outline: 0 !important; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:not([href]):not([class]) { + color: inherit; + text-decoration: none; +} + +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; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + -ms-overflow-style: scrollbar; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +[role="button"] { + cursor: pointer; +} + +select { + word-wrap: normal; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map new file mode 100644 index 00000000..e79cab0c --- /dev/null +++ b/src/Services/o2bionics-id/src/O2Bionics.Services.IdServer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap-reboot.scss","bootstrap-reboot.css","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_hover.scss"],"names":[],"mappings":"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","file":"bootstrap-reboot.css","sourcesContent":["/*!\n * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","/*!\n * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=\"button\"] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

    `-`

    ` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

    `s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-`