From a1029b4a286e8d3879588d8e52971a104a8d9bca Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Thu, 20 Mar 2025 14:02:43 +0100 Subject: [PATCH 01/15] frist --- .../OpenApi/OpenApi.Api1/OpenApi.Api1.csproj | 17 +++ BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http | 6 + BFF/v3/OpenApi/OpenApi.Api1/Program.cs | 45 +++++++ .../Properties/launchSettings.json | 23 ++++ .../OpenApi.Api1/appsettings.Development.json | 8 ++ BFF/v3/OpenApi/OpenApi.Api1/appsettings.json | 9 ++ .../OpenApi/OpenApi.Api2/OpenApi.Api2.csproj | 17 +++ BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http | 6 + BFF/v3/OpenApi/OpenApi.Api2/Program.cs | 45 +++++++ .../Properties/launchSettings.json | 23 ++++ .../OpenApi.Api2/appsettings.Development.json | 8 ++ BFF/v3/OpenApi/OpenApi.Api2/appsettings.json | 9 ++ .../OpenApi.DevServer.AppHost.csproj | 23 ++++ .../OpenApi.DevServer.AppHost/Program.cs | 10 ++ .../Properties/launchSettings.json | 29 +++++ .../appsettings.Development.json | 8 ++ .../appsettings.json | 9 ++ .../Extensions.cs | 119 ++++++++++++++++++ .../OpenApi.DevServer.ServiceDefaults.csproj | 22 ++++ BFF/v3/OpenApi/OpenApi.sln | 43 +++++++ 20 files changed, 479 insertions(+) create mode 100644 BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj create mode 100644 BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http create mode 100644 BFF/v3/OpenApi/OpenApi.Api1/Program.cs create mode 100644 BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.Api1/appsettings.Development.json create mode 100644 BFF/v3/OpenApi/OpenApi.Api1/appsettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj create mode 100644 BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http create mode 100644 BFF/v3/OpenApi/OpenApi.Api2/Program.cs create mode 100644 BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.Api2/appsettings.Development.json create mode 100644 BFF/v3/OpenApi/OpenApi.Api2/appsettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.Development.json create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj create mode 100644 BFF/v3/OpenApi/OpenApi.sln diff --git a/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj new file mode 100644 index 00000000..b9dd54b5 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + diff --git a/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http new file mode 100644 index 00000000..48f54fb2 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http @@ -0,0 +1,6 @@ +@OpenApi.Api1_HostAddress = http://localhost:5265 + +GET {{OpenApi.Api1_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/BFF/v3/OpenApi/OpenApi.Api1/Program.cs b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs new file mode 100644 index 00000000..f58e3185 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs @@ -0,0 +1,45 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast"); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json new file mode 100644 index 00000000..990d7d23 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5265", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7289;http://localhost:5265", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.Api1/appsettings.Development.json b/BFF/v3/OpenApi/OpenApi.Api1/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api1/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.Api1/appsettings.json b/BFF/v3/OpenApi/OpenApi.Api1/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api1/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj new file mode 100644 index 00000000..b9dd54b5 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + diff --git a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http new file mode 100644 index 00000000..bc39e2a9 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http @@ -0,0 +1,6 @@ +@OpenApi.Api2_HostAddress = http://localhost:5084 + +GET {{OpenApi.Api2_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/BFF/v3/OpenApi/OpenApi.Api2/Program.cs b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs new file mode 100644 index 00000000..f58e3185 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs @@ -0,0 +1,45 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast"); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json new file mode 100644 index 00000000..d05de986 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5084", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7297;http://localhost:5084", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.Api2/appsettings.Development.json b/BFF/v3/OpenApi/OpenApi.Api2/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api2/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.Api2/appsettings.json b/BFF/v3/OpenApi/OpenApi.Api2/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api2/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj new file mode 100644 index 00000000..94c9321f --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + net9.0 + enable + enable + true + 3ff4ee13-d005-4899-9f8f-62c60bfef868 + + + + + + + + + + + + diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs new file mode 100644 index 00000000..a7bf0358 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs @@ -0,0 +1,10 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddProject("openapi-api1"); + +builder.AddProject("openapi-api2"); + +builder.Build().Run(); diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..8b728174 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17275;http://localhost:15267", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21026", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22140" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15267", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19227", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20119" + } + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.Development.json b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.json b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..13151bf4 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj new file mode 100644 index 00000000..24b1b4fe --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/BFF/v3/OpenApi/OpenApi.sln b/BFF/v3/OpenApi/OpenApi.sln new file mode 100644 index 00000000..8980dfdd --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.DevServer.AppHost", "OpenApi.DevServer.AppHost\OpenApi.DevServer.AppHost.csproj", "{2684F8C2-978B-477E-A3D5-11D66A51BE49}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.DevServer.ServiceDefaults", "OpenApi.DevServer.ServiceDefaults\OpenApi.DevServer.ServiceDefaults.csproj", "{CCA873A3-82AD-11F0-6E1B-535304949BF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.Api1", "OpenApi.Api1\OpenApi.Api1.csproj", "{353821E1-3DB8-E3E6-2AB4-87077C96F429}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.Api2", "OpenApi.Api2\OpenApi.Api2.csproj", "{D2C78D85-6B20-2F26-0FAD-55590EBC680D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|Any CPU.Build.0 = Release|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|Any CPU.Build.0 = Release|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|Any CPU.Build.0 = Debug|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|Any CPU.ActiveCfg = Release|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|Any CPU.Build.0 = Release|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C00FBF94-02DF-468E-887B-74980E5DA673} + EndGlobalSection +EndGlobal From 6ef7fd8c355a90c5484ec50bd5c04ce4f8d00ae4 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 10:00:23 +0100 Subject: [PATCH 02/15] proxying works --- BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http | 2 +- BFF/v3/OpenApi/OpenApi.Api1/Program.cs | 8 +- BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http | 4 +- BFF/v3/OpenApi/OpenApi.Api2/Program.cs | 3 +- BFF/v3/OpenApi/OpenApi.Bff/Configuration.cs | 29 ++++ .../OpenApi.Bff/OpenApi.Api2.proxy.http | 8 + BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj | 19 +++ BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 69 ++++++++ .../Properties/launchSettings.json | 13 ++ .../OpenApi.Bff/appsettings.Development.json | 24 +++ BFF/v3/OpenApi/OpenApi.Bff/appsettings.json | 24 +++ .../OpenApi.Bff/wwwroot/StyleSheet.css | 7 + BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js | 157 ++++++++++++++++++ BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html | 69 ++++++++ .../wwwroot/libs/bootstrap.min.css | 5 + .../OpenApi.DevServer.AppHost.csproj | 2 + ...penApi.DevServer.AppHost.v3.ncrunchproject | 5 + .../OpenApi.DevServer.AppHost/Program.cs | 10 +- .../Extensions.cs | 56 +++++++ BFF/v3/OpenApi/OpenApi.sln | 50 ++++++ BFF/v3/OpenApi/OpenApi.v3.ncrunchsolution | 8 + 21 files changed, 561 insertions(+), 11 deletions(-) create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/Configuration.cs create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Api2.proxy.http create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/Program.cs create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/appsettings.Development.json create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/appsettings.json create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/wwwroot/StyleSheet.css create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/wwwroot/libs/bootstrap.min.css create mode 100644 BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.v3.ncrunchproject create mode 100644 BFF/v3/OpenApi/OpenApi.v3.ncrunchsolution diff --git a/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http index 48f54fb2..e57f9050 100644 --- a/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http +++ b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.http @@ -1,4 +1,4 @@ -@OpenApi.Api1_HostAddress = http://localhost:5265 +@OpenApi.Api1_HostAddress = https://localhost:7289 GET {{OpenApi.Api1_HostAddress}}/weatherforecast/ Accept: application/json diff --git a/BFF/v3/OpenApi/OpenApi.Api1/Program.cs b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs index f58e3185..c35f5b36 100644 --- a/BFF/v3/OpenApi/OpenApi.Api1/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs @@ -23,13 +23,13 @@ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -app.MapGet("/weatherforecast", () => +app.MapGet("/weatherforecastFahrenheit", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), + Random.Shared.Next(-4, 131), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); @@ -39,7 +39,7 @@ app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +record WeatherForecast(DateOnly Date, int TemperatureF, string? Summary) { - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } diff --git a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http index bc39e2a9..f512d322 100644 --- a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http +++ b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.http @@ -1,6 +1,6 @@ -@OpenApi.Api2_HostAddress = http://localhost:5084 +@OpenApi.Api2_HostAddress = https://localhost:7289 -GET {{OpenApi.Api2_HostAddress}}/weatherforecast/ +GET {{OpenApi.Api2_HostAddress}}/weatherforecastfahrenheit/ Accept: application/json ### diff --git a/BFF/v3/OpenApi/OpenApi.Api2/Program.cs b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs index f58e3185..7c7ec1c0 100644 --- a/BFF/v3/OpenApi/OpenApi.Api2/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs @@ -23,7 +23,7 @@ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -app.MapGet("/weatherforecast", () => +app.MapGet("/WeatherforecastCelcius", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast @@ -41,5 +41,4 @@ record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Configuration.cs b/BFF/v3/OpenApi/OpenApi.Bff/Configuration.cs new file mode 100644 index 00000000..237d5606 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/Configuration.cs @@ -0,0 +1,29 @@ +using Duende.Bff; + +namespace OpenApi.Bff; + +/// +/// Configuration section +/// +public class Configuration +{ + public string? Authority { get; set; } + + public string? ClientId { get; set; } + + /// + /// should be supplied as a command line argument or environment variable, e.g. + /// ./GenericBFF --BFF:ClientSecret=secret + /// + public string? ClientSecret { get; set; } + + public List Scopes { get; set; } = new(); + public List Apis { get; set; } = new(); +} + +public class Api +{ + public string? LocalPath { get; set; } + public string? RemoteUrl { get; set; } + public TokenType RequiredToken { get; set; } +} diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Api2.proxy.http b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Api2.proxy.http new file mode 100644 index 00000000..2711e061 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Api2.proxy.http @@ -0,0 +1,8 @@ +# For more info on HTTP files go to https://aka.ms/vs/httpfile +@OpenApi.Api2_HostAddress = https://localhost:7082/api1 + +GET {{OpenApi.Api2_HostAddress}}/weatherforecastfahrenheit/ +Accept: application/json +X-CSRF: 1 + +### diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj new file mode 100644 index 00000000..c75fb6d1 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs new file mode 100644 index 00000000..d35643c4 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -0,0 +1,69 @@ +using OpenApi.Bff; +using Duende.Bff.Yarp; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddBff() + .AddRemoteApis(); + +builder.Services.AddHttpForwarderWithServiceDiscovery(); + +Configuration config = new(); +builder.Configuration.Bind("BFF", config); + +builder.Services.AddAuthentication(options => + { + options.DefaultScheme = "cookie"; + options.DefaultChallengeScheme = "oidc"; + options.DefaultSignOutScheme = "oidc"; + }) + .AddCookie("cookie", options => + { + options.Cookie.Name = "__Host-bff"; + options.Cookie.SameSite = SameSiteMode.Strict; + }) + .AddOpenIdConnect("oidc", options => + { + options.Authority = config.Authority; + options.ClientId = config.ClientId; + options.ClientSecret = config.ClientSecret; + + options.ResponseType = "code"; + options.ResponseMode = "query"; + + options.GetClaimsFromUserInfoEndpoint = true; + options.MapInboundClaims = false; + options.SaveTokens = true; + + options.Scope.Clear(); + foreach (var scope in config.Scopes) + { + options.Scope.Add(scope); + } + + options.TokenValidationParameters = new() + { + NameClaimType = "name", + RoleClaimType = "role" + }; + }); + + +var app = builder.Build(); + +app.UseDefaultFiles(); +app.UseStaticFiles(); + +app.UseAuthentication(); +app.UseBff(); + +app.MapBffManagementEndpoints(); + +app.MapRemoteBffApiEndpoint("/api1", Services.Api1.LogicalUri().ToString()); +app.MapRemoteBffApiEndpoint("/api2", Services.Api2.LogicalUri().ToString()); +// .RequireAccessToken(api.RequiredToken); + + +app.Run(); diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json new file mode 100644 index 00000000..dddf26bb --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7082;http://localhost:5113", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.Bff/appsettings.Development.json b/BFF/v3/OpenApi/OpenApi.Bff/appsettings.Development.json new file mode 100644 index 00000000..0094f082 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/appsettings.Development.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + + "BFF": { + "Authority": "https://demo.duendesoftware.com", + "ClientId": "interactive.confidential", + "ClientSecret": "secret", + + "Scopes": [ "openid", "profile", "api", "offline_access" ], + + "Apis": [ + { + "LocalPath": "/api", + "RemoteUrl": "https://demo.duendesoftware.com/api/test", + "RequiredToken": "User" + } + ] + } +} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Bff/appsettings.json b/BFF/v3/OpenApi/OpenApi.Bff/appsettings.json new file mode 100644 index 00000000..a5fbe783 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + "BFF": { + "Authority": "https://demo.duendesoftware.com", + "ClientId": "interactive.confidential", + "ClientSecret": "secret", + "Scopes": [ "openid", "profile", "api", "offline_access" ], + + "Apis": [ + { + "LocalPath": "/api", + "RemoteUrl": "https://demo.duendesoftware.com/api/test", + "RequiredToken": "User" + } + ] + } +} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/StyleSheet.css b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/StyleSheet.css new file mode 100644 index 00000000..2c1e366b --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/StyleSheet.css @@ -0,0 +1,7 @@ +pre:empty { + display: none; +} + +iframe { + display:none; +} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js new file mode 100644 index 00000000..cde9cc3c --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js @@ -0,0 +1,157 @@ +const loginUrl = "/bff/login"; +const silentLoginUrl = "/bff/silent-login"; +const userUrl = "/bff/user"; +const apiUrl = "/api"; +let logoutUrl = "/bff/logout"; + +async function onLoad() { + var req = new Request(userUrl, { + headers: new Headers({ + 'X-CSRF': '1' + }) + }) + + try { + var resp = await fetch(req); + if (resp.ok) { + + let claims = await resp.json(); + showUser(claims); + + if (claims) { + log("user logged in"); + + let logoutUrlClaim = claims.find(claim => claim.type === 'bff:logout_url'); + if (logoutUrlClaim) { + logoutUrl = logoutUrlClaim.value; + } + } + else { + log("user not logged in"); + } + } else if (resp.status === 401) { + log("user not logged in"); + + // if we've detected that the user is no already logged in, we can attempt a silent login + // this will trigger a normal OIDC request in an iframe using prompt=none. + // if the user is already logged into IdentityServer, then the result will establish a session in the BFF. + // this whole process avoids redirecting the top window without knowing if the user is logged in or not. + var silentLoginResult = await silentLogin(); + + // the result is a boolean letting us know if the user has been logged in silently + log("silent login result: " + silentLoginResult); + + if (silentLoginResult) { + // if we now have a user logged in silently, then reload this window + window.location.reload(); + } + } + } + catch (e) { + log("error checking user status"); + } +} + +onLoad(); + +function login() { + window.location = loginUrl; +} + +function logout() { + window.location = logoutUrl; +} + +async function callApi() { + var req = new Request(apiUrl, { + headers: new Headers({ + 'X-CSRF': '1' + }) + }) + var resp = await fetch(req); + + log("API Result: " + resp.status); + if (resp.ok) { + showApi(await resp.json()); + } +} + + +document.querySelector(".login").addEventListener("click", login, false); +document.querySelector(".call_api").addEventListener("click", callApi, false); +document.querySelector(".logout").addEventListener("click", logout, false); + + +function showApi() { + document.getElementById('api-result').innerText = ''; + + Array.prototype.forEach.call(arguments, function (msg) { + if (msg instanceof Error) { + msg = "Error: " + msg.message; + } else if (typeof msg !== 'string') { + msg = JSON.stringify(msg, null, 2); + } + document.getElementById('api-result').innerText += msg + '\r\n'; + }); +} + +function showUser() { + document.getElementById('user').innerText = ''; + + Array.prototype.forEach.call(arguments, function (msg) { + if (msg instanceof Error) { + msg = "Error: " + msg.message; + } else if (typeof msg !== 'string') { + msg = JSON.stringify(msg, null, 2); + } + document.getElementById('user').innerText += msg + '\r\n'; + }); +} + +function log() { + document.getElementById('response').innerText = ''; + + Array.prototype.forEach.call(arguments, function (msg) { + if (msg instanceof Error) { + msg = "Error: " + msg.message; + } else if (typeof msg !== 'string') { + msg = JSON.stringify(msg, null, 2); + } + document.getElementById('response').innerText += msg + '\r\n'; + }); +} + + +// this will trigger the silent login and return a promise that resolves to true or false. +function silentLogin(iframeSelector) { + iframeSelector = iframeSelector || "#bff-silent-login"; + const timeout = 5000; + + return new Promise((resolve, reject) => { + function onMessage(e) { + // look for messages sent from the BFF iframe + if (e.data && e.data['source'] === 'bff-silent-login') { + window.removeEventListener("message", onMessage); + // send along the boolean result + resolve(e.data.isLoggedIn); + } + }; + + // listen for the iframe response to notify its parent (i.e. this window). + window.addEventListener("message", onMessage); + + // we're setting up a time to handle scenarios when the iframe doesn't return immediaetly (despite prompt=none). + // this likely means the iframe is showing the error page at IdentityServer (typically due to client misconfiguration). + window.setTimeout(() => { + window.removeEventListener("message", onMessage); + + // we can either just treat this like a "not logged in" + resolve(false); + // or we can trigger an error, so someone can look into the reason why + // reject(new Error("timed_out")); + }, timeout); + + // send the iframe to the silent login endpoint to kick off the workflow + document.querySelector(iframeSelector).src = silentLoginUrl; + }); +} diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html new file mode 100644 index 00000000..1379544d --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html @@ -0,0 +1,69 @@ + + + + + + + + + +
+ + +
+
    +
  • Home
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ +
+
    +
+
+ +
+
+
Message
+
+

+            
+
+
+ +
+
+
+
Current User
+
+

+                
+
+
+ +
+
+
API Result
+
+

+                
+
+
+
+
+ + + + + + diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/libs/bootstrap.min.css b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/libs/bootstrap.min.css new file mode 100644 index 00000000..28f154de --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/libs/bootstrap.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.2 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.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-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-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-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.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{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.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{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.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{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px \9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.form-group-sm .form-control{height:30px;line-height:30px}select[multiple].form-group-sm .form-control,textarea.form-group-sm .form-control{height:auto}.form-group-sm .form-control-static{height:30px;padding:5px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.form-group-lg .form-control{height:46px;line-height:46px}select[multiple].form-group-lg .form-control,textarea.form-group-lg .form-control{height:auto}.form-group-lg .form-control-static{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.33px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.active,.btn-default.focus,.btn-default:active,.btn-default:focus,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.active,.btn-primary.focus,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.active,.btn-success.focus,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.active,.btn-info.focus,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.active,.btn-warning.focus,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.active,.btn-danger.focus,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none;visibility:hidden}.collapse.in{display:block;visibility:visible}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none;visibility:hidden}.tab-content>.active{display:block;visibility:visible}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important;visibility:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:absolute;top:0;right:0;left:0;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-weight:400;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.42857143;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj index 94c9321f..baabf6c5 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.csproj @@ -18,6 +18,8 @@ + + diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.v3.ncrunchproject b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.v3.ncrunchproject new file mode 100644 index 00000000..319cd523 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/OpenApi.DevServer.AppHost.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs index a7bf0358..5b526710 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs @@ -1,10 +1,16 @@ // Copyright (c) Duende Software. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using Microsoft.Extensions.Hosting; + var builder = DistributedApplication.CreateBuilder(args); -builder.AddProject("openapi-api1"); +var api1 = builder.AddProject(Services.Api1.ToString()); + +var api2 = builder.AddProject(Services.Api2.ToString()); +var bff = builder.AddProject(Services.Bff.ToString()); -builder.AddProject("openapi-api2"); +bff.WithReference(api1) + .WithReference(api2); builder.Build().Run(); diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs index 13151bf4..7c38b644 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs +++ b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs @@ -1,3 +1,5 @@ +using System.Collections; +using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; @@ -117,3 +119,57 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) return app; } } + +public static class Services +{ + public static readonly Endpoint Api1 = UsePropertyName(); + public static readonly Endpoint Api2 = UsePropertyName(); + public static readonly Endpoint Bff = UsePropertyName(); + + /// + /// Build the name of the property from the caller's name. + /// + /// + /// + /// + private static Endpoint UsePropertyName([CallerMemberName] string? name = null) => + new Endpoint(name ?? throw new ArgumentNullException()); +} + +public class Endpoint(string name) +{ + public Uri LogicalUri() => new Uri("https://" + name); + public Uri ActualUri() => ServiceDiscovery.ResolveUri(this); + + public override string ToString() => name; +} + +public static class ServiceDiscovery +{ + public static Uri ResolveUri(Endpoint endpoint) + { + var uri = endpoint.LogicalUri(); + var scheme = uri.Scheme; + var resolvedUri = Environment.GetEnvironmentVariable($"services__{endpoint}__{scheme}__0"); + + if (resolvedUri == null) + { + var envVars = Environment.GetEnvironmentVariables(); + var servicesEnvVars = new Dictionary(); + + foreach (DictionaryEntry entry in envVars) + { + var key = entry.Key.ToString(); + if (key != null && key.StartsWith("services__")) + { + servicesEnvVars[key] = entry.Value?.ToString() ?? string.Empty; + } + } + + var availableServices = string.Join(", ", servicesEnvVars.Select(kv => $"{kv.Key}={kv.Value}")); + throw new InvalidOperationException($"Service {endpoint} not found in service discovery. Available are: " + availableServices); + } + + return new Uri(resolvedUri); + } +} diff --git a/BFF/v3/OpenApi/OpenApi.sln b/BFF/v3/OpenApi/OpenApi.sln index 8980dfdd..2456c1af 100644 --- a/BFF/v3/OpenApi/OpenApi.sln +++ b/BFF/v3/OpenApi/OpenApi.sln @@ -11,28 +11,78 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.Api1", "OpenApi.Api EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.Api2", "OpenApi.Api2\OpenApi.Api2.csproj", "{D2C78D85-6B20-2F26-0FAD-55590EBC680D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.Bff", "OpenApi.Bff\OpenApi.Bff.csproj", "{2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|x64.ActiveCfg = Debug|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|x64.Build.0 = Debug|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|x86.ActiveCfg = Debug|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Debug|x86.Build.0 = Debug|Any CPU {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|Any CPU.ActiveCfg = Release|Any CPU {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|Any CPU.Build.0 = Release|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|x64.ActiveCfg = Release|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|x64.Build.0 = Release|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|x86.ActiveCfg = Release|Any CPU + {2684F8C2-978B-477E-A3D5-11D66A51BE49}.Release|x86.Build.0 = Release|Any CPU {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|x64.ActiveCfg = Debug|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|x64.Build.0 = Debug|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Debug|x86.Build.0 = Debug|Any CPU {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|Any CPU.Build.0 = Release|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|x64.ActiveCfg = Release|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|x64.Build.0 = Release|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|x86.ActiveCfg = Release|Any CPU + {CCA873A3-82AD-11F0-6E1B-535304949BF5}.Release|x86.Build.0 = Release|Any CPU {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|Any CPU.Build.0 = Debug|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|x64.ActiveCfg = Debug|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|x64.Build.0 = Debug|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|x86.ActiveCfg = Debug|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Debug|x86.Build.0 = Debug|Any CPU {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|Any CPU.ActiveCfg = Release|Any CPU {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|Any CPU.Build.0 = Release|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|x64.ActiveCfg = Release|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|x64.Build.0 = Release|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|x86.ActiveCfg = Release|Any CPU + {353821E1-3DB8-E3E6-2AB4-87077C96F429}.Release|x86.Build.0 = Release|Any CPU {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|x64.Build.0 = Debug|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Debug|x86.Build.0 = Debug|Any CPU {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|Any CPU.Build.0 = Release|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|x64.ActiveCfg = Release|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|x64.Build.0 = Release|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|x86.ActiveCfg = Release|Any CPU + {D2C78D85-6B20-2F26-0FAD-55590EBC680D}.Release|x86.Build.0 = Release|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Debug|x64.Build.0 = Debug|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Debug|x86.Build.0 = Debug|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|Any CPU.Build.0 = Release|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|x64.ActiveCfg = Release|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|x64.Build.0 = Release|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|x86.ActiveCfg = Release|Any CPU + {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BFF/v3/OpenApi/OpenApi.v3.ncrunchsolution b/BFF/v3/OpenApi/OpenApi.v3.ncrunchsolution new file mode 100644 index 00000000..13107d39 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.v3.ncrunchsolution @@ -0,0 +1,8 @@ + + + True + True + True + True + + \ No newline at end of file From 6dcc4b759b6702920471d2a5716d0ed79ebe4671 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 10:09:32 +0100 Subject: [PATCH 03/15] add basic swagger ui --- BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj | 3 ++- BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj index c75fb6d1..c44e0ff6 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -9,6 +9,7 @@ + diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index d35643c4..cd7d0a57 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -1,5 +1,6 @@ using OpenApi.Bff; using Duende.Bff.Yarp; +using Swashbuckle.AspNetCore.SwaggerUI; var builder = WebApplication.CreateBuilder(args); @@ -10,6 +11,8 @@ builder.Services.AddHttpForwarderWithServiceDiscovery(); + + Configuration config = new(); builder.Configuration.Bind("BFF", config); @@ -56,6 +59,12 @@ app.UseDefaultFiles(); app.UseStaticFiles(); +app.Use(async (c, n) => +{ + c.Request.Headers.Add("X-CSRF", "1"); + await n(); +}); + app.UseAuthentication(); app.UseBff(); @@ -64,6 +73,10 @@ app.MapRemoteBffApiEndpoint("/api1", Services.Api1.LogicalUri().ToString()); app.MapRemoteBffApiEndpoint("/api2", Services.Api2.LogicalUri().ToString()); // .RequireAccessToken(api.RequiredToken); - +app.UseSwaggerUI(c => +{ + c.SwaggerEndpoint("../api1/openapi/v1.json", "Api1"); + c.SwaggerEndpoint("../api2/openapi/v1.json", "Api2"); +}); app.Run(); From 097d51dd484155ea062962a0439c87c18dd7c2ac Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 11:13:24 +0100 Subject: [PATCH 04/15] added transforms --- BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj | 3 + BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 76 ++++++++++++++++++- .../OpenApi.DevServer.AppHost/Program.cs | 4 +- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj index c44e0ff6..34b90753 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj @@ -9,7 +9,10 @@ + + + diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index cd7d0a57..37895ee2 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -1,6 +1,17 @@ using OpenApi.Bff; using Duende.Bff.Yarp; using Swashbuckle.AspNetCore.SwaggerUI; +using Yarp.ReverseProxy.Transforms; +using Microsoft.AspNetCore.Http.Features; +using System.Text.Json.Nodes; +using System.Text.Json; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Writers; +using System.IO; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Readers; var builder = WebApplication.CreateBuilder(args); @@ -53,6 +64,12 @@ }; }); +builder.Services.AddSingleton((path, c) => +{ + DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(path, c); + c.ResponseTransforms.Add(new OpenApiResponseTransform(path)); +}); + var app = builder.Build(); @@ -61,7 +78,10 @@ app.Use(async (c, n) => { - c.Request.Headers.Add("X-CSRF", "1"); + if (c.Request.Path.ToString().EndsWith("/openapi/v1.json")) + { + c.Request.Headers.Add("X-CSRF", "1"); + } await n(); }); @@ -80,3 +100,57 @@ }); app.Run(); + +public class OpenApiResponseTransform(string basePath) : ResponseTransform +{ + public override async ValueTask ApplyAsync(ResponseTransformContext context) + { + // Check if the request path matches /openapi/{document}.json + if (context.HttpContext.Request.Path.StartsWithSegments(basePath +"/openapi", out var remainingPath) && + remainingPath.HasValue && remainingPath.Value.EndsWith(".json")) + { + var readAsStreamAsync = await context.ProxyResponse.Content.ReadAsStreamAsync(); + var doc = new OpenApiStreamReader().Read(readAsStreamAsync, out var diagnostic); + context.SuppressResponseBody = true; + + + doc.Servers.Clear(); + doc.Servers.Add(new OpenApiServer() + { + Url = new Uri(Services.Bff.ActualUri(), basePath).ToString() + }); + foreach(var path in doc.Paths) + { + foreach (var operation in path.Value.Operations) + { + operation.Value.Responses.Add("401", new OpenApiResponse() + { + Description = "Unauthorized" + }); + operation.Value.Parameters.Add(new OpenApiParameter() + { + In = ParameterLocation.Header, + Name = "X-CSRF", + Required = true, + Schema = new OpenApiSchema() + { + Type = "string", + Default = new OpenApiString("1") + } + + }); + } + } + // Read and parse the existing JSON content + + var memoryStream = new MemoryStream(); + doc.Serialize(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); + memoryStream.Position = 0; + await memoryStream.CopyToAsync(context.HttpContext.Response.Body); + await context.HttpContext.Response.Body.FlushAsync(); + + } + } +} + + diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs index 5b526710..2f0674e3 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Program.cs @@ -11,6 +11,8 @@ var bff = builder.AddProject(Services.Bff.ToString()); bff.WithReference(api1) - .WithReference(api2); + .WithReference(api2) + .WithReference(bff) + ; builder.Build().Run(); From f265a5c89761b872a789b380f67b30a518da75c6 Mon Sep 17 00:00:00 2001 From: khalidabuhakmeh Date: Fri, 21 Mar 2025 08:04:38 -0400 Subject: [PATCH 05/15] Update Swagger endpoint paths to use absolute URLs Changed relative Swagger endpoint paths to absolute ones for consistency and to prevent potential issues with path resolution. This ensures the Swagger UI functions correctly in all deployment environments. --- BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index 37895ee2..df7f75f1 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -95,8 +95,8 @@ // .RequireAccessToken(api.RequiredToken); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("../api1/openapi/v1.json", "Api1"); - c.SwaggerEndpoint("../api2/openapi/v1.json", "Api2"); + c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); + c.SwaggerEndpoint("/api2/openapi/v1.json", "Api2"); }); app.Run(); From ca8c146ac6725466ec8e13f041199525ff546147 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 13:09:52 +0100 Subject: [PATCH 06/15] auth --- BFF/v3/OpenApi/OpenApi.Api1/Program.cs | 7 +- .../OpenApi/OpenApi.Api2/OpenApi.Api2.csproj | 1 - BFF/v3/OpenApi/OpenApi.Api2/Program.cs | 9 +-- BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 20 +++--- .../wwwroot/swagger/swagger-bff-login.js | 25 +++++++ .../Extensions.cs | 72 +++++++++++++++++++ .../OpenApi.DevServer.ServiceDefaults.csproj | 4 ++ 7 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js diff --git a/BFF/v3/OpenApi/OpenApi.Api1/Program.cs b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs index c35f5b36..61219e2b 100644 --- a/BFF/v3/OpenApi/OpenApi.Api1/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs @@ -2,9 +2,8 @@ builder.AddServiceDefaults(); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +builder.AddDefaultAuthentication("api"); +builder.AddDefaultOpenApiConfig(); var app = builder.Build(); @@ -35,6 +34,8 @@ .ToArray(); return forecast; }) +.RequireAuthorization("ApiScope") + .WithName("GetWeatherForecast"); app.Run(); diff --git a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj index b9dd54b5..28d03f7f 100644 --- a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj +++ b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj @@ -7,7 +7,6 @@ - diff --git a/BFF/v3/OpenApi/OpenApi.Api2/Program.cs b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs index 7c7ec1c0..70cb16d2 100644 --- a/BFF/v3/OpenApi/OpenApi.Api2/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs @@ -2,9 +2,9 @@ builder.AddServiceDefaults(); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +builder.AddDefaultAuthentication("api"); + +builder.AddDefaultOpenApiConfig(); var app = builder.Build(); @@ -35,7 +35,8 @@ .ToArray(); return forecast; }) -.WithName("GetWeatherForecast"); +.WithName("GetWeatherForecast").RequireAuthorization("ApiScope"); +; app.Run(); diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index df7f75f1..490de0bb 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -1,15 +1,9 @@ using OpenApi.Bff; using Duende.Bff.Yarp; -using Swashbuckle.AspNetCore.SwaggerUI; using Yarp.ReverseProxy.Transforms; -using Microsoft.AspNetCore.Http.Features; -using System.Text.Json.Nodes; -using System.Text.Json; using Microsoft.OpenApi; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Extensions; -using Microsoft.OpenApi.Writers; -using System.IO; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Readers; @@ -90,11 +84,14 @@ app.MapBffManagementEndpoints(); -app.MapRemoteBffApiEndpoint("/api1", Services.Api1.LogicalUri().ToString()); -app.MapRemoteBffApiEndpoint("/api2", Services.Api2.LogicalUri().ToString()); +app.MapRemoteBffApiEndpoint("/api1", Services.Api1.LogicalUri().ToString()) + .RequireAccessToken(); +app.MapRemoteBffApiEndpoint("/api2", Services.Api2.LogicalUri().ToString()) + .RequireAccessToken(); ; // .RequireAccessToken(api.RequiredToken); app.UseSwaggerUI(c => { + c.InjectJavascript("swagger-bff-login.js"); c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); c.SwaggerEndpoint("/api2/openapi/v1.json", "Api2"); }); @@ -119,7 +116,12 @@ public override async ValueTask ApplyAsync(ResponseTransformContext context) { Url = new Uri(Services.Bff.ActualUri(), basePath).ToString() }); - foreach(var path in doc.Paths) + + // remove the jwt security scheme + doc.Components.SecuritySchemes.Clear(); + + + foreach (var path in doc.Paths) { foreach (var operation in path.Value.Operations) { diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js new file mode 100644 index 00000000..ecb242fd --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js @@ -0,0 +1,25 @@ +function addLoginButton() { + const buttonContainer = document.querySelector(".topbar"); + if (!buttonContainer) { + }; + + // Create Login Button + const loginButton = document.createElement("button"); + loginButton.textContent = "Login"; + loginButton.style.marginLeft = "10px"; + loginButton.style.padding = "8px 12px"; + loginButton.style.borderRadius = "5px"; + loginButton.style.border = "none"; + loginButton.style.background = "#007bff"; + loginButton.style.color = "white"; + loginButton.style.cursor = "pointer"; + + loginButton.onclick = function () { + window.location.href = "/bff/login?returnUrl=/swagger/index.html"; // Redirect to login endpoint + }; + + buttonContainer.appendChild(loginButton); +} + +// Wait for Swagger UI to load and add the button +setTimeout(addLoginButton, 500); diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs index 7c38b644..ed83c3d5 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs +++ b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs @@ -1,11 +1,14 @@ using System.Collections; using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery; +using Microsoft.OpenApi.Models; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -72,6 +75,47 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w return builder; } + public static TBuilder AddDefaultOpenApiConfig(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + // Add services to the container. + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi + builder.Services.AddOpenApi(options => + options.AddDocumentTransformer() + ); + + return builder; + } + + public static TBuilder AddDefaultAuthentication(this TBuilder builder, string a) + where TBuilder : IHostApplicationBuilder + { + // Add JWT authentication services + builder.Services.AddAuthentication("Bearer") + .AddJwtBearer("Bearer", options => + { + options.Authority = "https://demo.duendesoftware.com"; + options.Audience = "api"; + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true + }; + }); + + builder.Services.AddAuthorization(options => + { + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "api2"); + }); + }); + + return builder; + } private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder { var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); @@ -118,6 +162,34 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app) return app; } + + +} + + +internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) + : IOpenApiDocumentTransformer +{ + public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, + CancellationToken cancellationToken) + { + var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync(); + if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer")) + { + var requirements = new Dictionary + { + ["Bearer"] = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", // "bearer" refers to the header name here + In = ParameterLocation.Header, + BearerFormat = "Json Web Token" + } + }; + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes = requirements; + } + } } public static class Services diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj index 24b1b4fe..e64ab6f5 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj +++ b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/OpenApi.DevServer.ServiceDefaults.csproj @@ -17,6 +17,10 @@ + + + + From 6e7e430ec7e165f8fd4129875fec9d49982461c0 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 14:30:45 +0100 Subject: [PATCH 07/15] swagger UI sample works --- BFF/v3/OpenApi/OpenApi.Api1/Program.cs | 6 +- BFF/v3/OpenApi/OpenApi.Api2/Program.cs | 4 +- BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 68 ++++++++++++------- .../Properties/launchSettings.json | 4 +- .../wwwroot/swagger/bff-auth-button.js | 43 ++++++++++++ .../wwwroot/swagger/swagger-bff-login.js | 25 ------- .../Extensions.cs | 23 ++++--- 7 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js delete mode 100644 BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js diff --git a/BFF/v3/OpenApi/OpenApi.Api1/Program.cs b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs index 61219e2b..2626a01e 100644 --- a/BFF/v3/OpenApi/OpenApi.Api1/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Api1/Program.cs @@ -2,7 +2,7 @@ builder.AddServiceDefaults(); -builder.AddDefaultAuthentication("api"); +builder.AddDefaultAuthentication(); builder.AddDefaultOpenApiConfig(); var app = builder.Build(); @@ -22,7 +22,7 @@ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -app.MapGet("/weatherforecastFahrenheit", () => +app.MapGet("/weatherforecastFahrenheit", (HttpContext c) => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast @@ -34,7 +34,7 @@ .ToArray(); return forecast; }) -.RequireAuthorization("ApiScope") +.RequireAuthorization() .WithName("GetWeatherForecast"); diff --git a/BFF/v3/OpenApi/OpenApi.Api2/Program.cs b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs index 70cb16d2..09b1aa94 100644 --- a/BFF/v3/OpenApi/OpenApi.Api2/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Api2/Program.cs @@ -2,7 +2,7 @@ builder.AddServiceDefaults(); -builder.AddDefaultAuthentication("api"); +builder.AddDefaultAuthentication(); builder.AddDefaultOpenApiConfig(); @@ -35,7 +35,7 @@ .ToArray(); return forecast; }) -.WithName("GetWeatherForecast").RequireAuthorization("ApiScope"); +.WithName("GetWeatherForecast").RequireAuthorization(); ; app.Run(); diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index 490de0bb..c6d2adaa 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -6,6 +6,8 @@ using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Readers; +using Duende.Bff; +using Microsoft.AspNetCore.Authorization; var builder = WebApplication.CreateBuilder(args); @@ -31,6 +33,7 @@ { options.Cookie.Name = "__Host-bff"; options.Cookie.SameSite = SameSiteMode.Strict; + options.LoginPath = "/bff/login"; }) .AddOpenIdConnect("oidc", options => { @@ -58,6 +61,11 @@ }; }); +builder.Services.AddAuthorization(opt => +{ + opt.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); +}); + builder.Services.AddSingleton((path, c) => { DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(path, c); @@ -72,10 +80,10 @@ app.Use(async (c, n) => { - if (c.Request.Path.ToString().EndsWith("/openapi/v1.json")) - { - c.Request.Headers.Add("X-CSRF", "1"); - } + //if (c.Request.Path.ToString().EndsWith("/openapi/v1.json")) + //{ + // c.Request.Headers"X-CSRF", "1"); + //} await n(); }); @@ -85,13 +93,16 @@ app.MapBffManagementEndpoints(); app.MapRemoteBffApiEndpoint("/api1", Services.Api1.LogicalUri().ToString()) - .RequireAccessToken(); + .WithOptionalUserAccessToken(); app.MapRemoteBffApiEndpoint("/api2", Services.Api2.LogicalUri().ToString()) - .RequireAccessToken(); ; -// .RequireAccessToken(api.RequiredToken); + .WithOptionalUserAccessToken(); + +app.UseAuthorization(); app.UseSwaggerUI(c => { - c.InjectJavascript("swagger-bff-login.js"); + c.UseRequestInterceptor("function(request){ request.headers['X-CSRF'] = '1';return request;}"); + + c.InjectJavascript("bff-auth-button.js"); c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); c.SwaggerEndpoint("/api2/openapi/v1.json", "Api2"); }); @@ -119,28 +130,35 @@ public override async ValueTask ApplyAsync(ResponseTransformContext context) // remove the jwt security scheme doc.Components.SecuritySchemes.Clear(); - + // Add the cookie security scheme + //doc.Components.SecuritySchemes.Add("cookieAuth", new OpenApiSecurityScheme + //{ + // Type = SecuritySchemeType.Http, + // In = ParameterLocation.Cookie, + // Name = "__Host-bff", + // Description = "Cookie-based authentication" + //}); foreach (var path in doc.Paths) { foreach (var operation in path.Value.Operations) { - operation.Value.Responses.Add("401", new OpenApiResponse() - { - Description = "Unauthorized" - }); - operation.Value.Parameters.Add(new OpenApiParameter() - { - In = ParameterLocation.Header, - Name = "X-CSRF", - Required = true, - Schema = new OpenApiSchema() - { - Type = "string", - Default = new OpenApiString("1") - } - - }); + //operation.Value.Responses.Add("401", new OpenApiResponse() + //{ + // Description = "Unauthorized" + //}); + //operation.Value.Parameters.Add(new OpenApiParameter() + //{ + // In = ParameterLocation.Header, + // Name = "X-CSRF", + // Required = true, + // Schema = new OpenApiSchema() + // { + // Type = "string", + // Default = new OpenApiString("1") + // } + + //}); } } // Read and parse the existing JSON content diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json index dddf26bb..a81c40d0 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json +++ b/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json @@ -1,9 +1,9 @@ -{ +{ "profiles": { "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "https://localhost:7082;http://localhost:5113", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js new file mode 100644 index 00000000..72b3e272 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js @@ -0,0 +1,43 @@ + +async function checkAuthentication() { + try { + let response = await fetch('/bff/user', { credentials: 'include', headers: {"x-csrf": 1} }); + + let wrapper = document.querySelector('.schemes.wrapper'); + + wrapper.insertAdjacentHTML('beforeend', `
`); + + let authorizeButton = document.querySelector('.swagger-ui .auth-wrapper .btn'); + if (!authorizeButton) return; + + if (response.status === 401) { + // User is unauthenticated + authorizeButton.innerText = "Log in"; + authorizeButton.onclick = function (event) { + event.preventDefault(); + window.location.href = "/bff/login?returnUrl=" + window.location.pathname; + }; + + } else if (response.status === 200) { + + let claims = await response.json(); + + let logoutUrlClaim = claims.find(claim => claim.type === 'bff:logout_url'); + if (logoutUrlClaim) { + authorizeButton.onclick = function (event) { + event.preventDefault(); + window.location.href = `${logoutUrlClaim.value}&returnUrl=${window.location.pathname}`; + }; + } + // User is authenticated + authorizeButton.innerText = "Log out"; + authorizeButton.classList.add("btn-success"); // Optional: Change button style + + } + } catch (error) { + console.error("Error checking authentication:", error); + } +} + +setTimeout(checkAuthentication, 1000); // Delay to ensure UI is loaded + diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js deleted file mode 100644 index ecb242fd..00000000 --- a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/swagger-bff-login.js +++ /dev/null @@ -1,25 +0,0 @@ -function addLoginButton() { - const buttonContainer = document.querySelector(".topbar"); - if (!buttonContainer) { - }; - - // Create Login Button - const loginButton = document.createElement("button"); - loginButton.textContent = "Login"; - loginButton.style.marginLeft = "10px"; - loginButton.style.padding = "8px 12px"; - loginButton.style.borderRadius = "5px"; - loginButton.style.border = "none"; - loginButton.style.background = "#007bff"; - loginButton.style.color = "white"; - loginButton.style.cursor = "pointer"; - - loginButton.onclick = function () { - window.location.href = "/bff/login?returnUrl=/swagger/index.html"; // Redirect to login endpoint - }; - - buttonContainer.appendChild(loginButton); -} - -// Wait for Swagger UI to load and add the button -setTimeout(addLoginButton, 500); diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs index ed83c3d5..078e5bf2 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs +++ b/BFF/v3/OpenApi/OpenApi.DevServer.ServiceDefaults/Extensions.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.OpenApi; @@ -87,7 +88,7 @@ public static TBuilder AddDefaultOpenApiConfig(this TBuilder builder) return builder; } - public static TBuilder AddDefaultAuthentication(this TBuilder builder, string a) + public static TBuilder AddDefaultAuthentication(this TBuilder builder) where TBuilder : IHostApplicationBuilder { // Add JWT authentication services @@ -95,23 +96,23 @@ public static TBuilder AddDefaultAuthentication(this TBuilder builder, .AddJwtBearer("Bearer", options => { options.Authority = "https://demo.duendesoftware.com"; - options.Audience = "api"; options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true + ValidateAudience = false, + ValidTypes = new[] { "at+jwt" }, + + NameClaimType = "name", + RoleClaimType = "role" }; }); builder.Services.AddAuthorization(options => { - options.AddPolicy("ApiScope", policy => - { - policy.RequireAuthenticatedUser(); - policy.RequireClaim("scope", "api2"); - }); + options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + //options.AddPolicy("ApiScope", policy => + //{ + // policy.RequireAuthenticatedUser(); + //}); }); return builder; From 2e62df99c9cca92871c4fecddf5d3b3718153cc6 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 14:35:52 +0100 Subject: [PATCH 08/15] use observer --- BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 1 + .../wwwroot/swagger/bff-auth-button.js | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index c6d2adaa..3396e72f 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -100,6 +100,7 @@ app.UseAuthorization(); app.UseSwaggerUI(c => { + c.UseRequestInterceptor("function(request){ request.headers['X-CSRF'] = '1';return request;}"); c.InjectJavascript("bff-auth-button.js"); diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js index 72b3e272..2de13b00 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js @@ -1,7 +1,6 @@ - async function checkAuthentication() { try { - let response = await fetch('/bff/user', { credentials: 'include', headers: {"x-csrf": 1} }); + let response = await fetch('/bff/user', { credentials: 'include', headers: { "x-csrf": 1 } }); let wrapper = document.querySelector('.schemes.wrapper'); @@ -39,5 +38,15 @@ async function checkAuthentication() { } } -setTimeout(checkAuthentication, 1000); // Delay to ensure UI is loaded +const observer = new MutationObserver((mutations, obs) => { + const wrapper = document.querySelector('.schemes.wrapper'); + if (wrapper) { + checkAuthentication(); + obs.disconnect(); + } +}); +observer.observe(document, { + childList: true, + subtree: true +}); From 1f5cd33be872faa60bbe546db71e57b12656b351 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 14:58:12 +0100 Subject: [PATCH 09/15] added some comments --- .../OpenApi.Bff/OpenApiResponseTransform.cs | 88 +++++++++++++++++ BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 99 ++----------------- 2 files changed, 95 insertions(+), 92 deletions(-) create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs b/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs new file mode 100644 index 00000000..620600b6 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs @@ -0,0 +1,88 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Microsoft.OpenApi; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using Yarp.ReverseProxy.Transforms; + +namespace OpenApi.Bff; + +/// +/// Transform the openapi document as it's being streamed. +/// +/// +public class OpenApiResponseTransform(string basePath) : ResponseTransform +{ + private static readonly OpenApiStreamReader OpenApiStreamReader = new OpenApiStreamReader(); + + public override async ValueTask ApplyAsync(ResponseTransformContext context) + { + // Check if the request path matches /openapi/{document}.json / .yaml + if (ProxyingOpenApiDocument(context)) + { + if (context.ProxyResponse == null) + // nothing to do if no response from the proxy + return; + + var openApiDocumentStream = await context.ProxyResponse.Content.ReadAsStreamAsync(); + var doc = OpenApiStreamReader.Read(openApiDocumentStream, out var diagnostic); + + // This line is needed because we're going to modify the output stream. + // If we don't do this, it's going to send both the original and the modified stream. + context.SuppressResponseBody = true; + + // Make sure the server is actually the BFF, not the original urls. + // All traffic is supposed to go through the bff. + doc.Servers.Clear(); + doc.Servers.Add(new OpenApiServer() + { + //Url = new Uri(Services.Bff.ActualUri(), basePath).ToString() + Url = Services.Bff.ActualUri().ToString() + }); + + // We remove the JWT security scheme, because the BFF changes the way + // the auth works to Cookie only. Specifying the cookie in the openapi document + // isn't useful. + doc.Components.SecuritySchemes.Clear(); + + // Combine the pathbase with the paths in the document, so that the + // paths are correct for the proxy. + var allPaths = doc.Paths.ToArray(); + doc.Paths.Clear(); + foreach (var path in allPaths) + { + doc.Paths[basePath + path.Key] = path.Value; + } + + await WriteDocumentToResponseStream(context, doc); + } + } + + private static async Task WriteDocumentToResponseStream(ResponseTransformContext context, OpenApiDocument doc) + { + var memoryStream = new MemoryStream(); + doc.Serialize(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); + memoryStream.Position = 0; + await memoryStream.CopyToAsync(context.HttpContext.Response.Body); + await context.HttpContext.Response.Body.FlushAsync(); + } + + private void AddPathBaseToAllPaths(OpenApiDocument doc) + { + var pathClones = doc.Paths.ToArray(); + doc.Paths.Clear(); + + foreach (var path in pathClones) + { + doc.Paths[basePath + path.Key] = path.Value; + } + } + + private bool ProxyingOpenApiDocument(ResponseTransformContext context) + { + return context.HttpContext.Request.Path.StartsWithSegments(basePath +"/openapi", out var remainingPath) && + remainingPath.HasValue && (remainingPath.Value.EndsWith(".json") || remainingPath.Value.EndsWith(".yaml")); + } +} diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index 3396e72f..f4aec1a4 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -1,13 +1,6 @@ using OpenApi.Bff; using Duende.Bff.Yarp; -using Yarp.ReverseProxy.Transforms; -using Microsoft.OpenApi; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Extensions; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Readers; -using Duende.Bff; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(args); @@ -16,10 +9,9 @@ builder.Services.AddBff() .AddRemoteApis(); +// Make sure Yarp understands aspire's service discovery. builder.Services.AddHttpForwarderWithServiceDiscovery(); - - Configuration config = new(); builder.Configuration.Bind("BFF", config); @@ -61,11 +53,6 @@ }; }); -builder.Services.AddAuthorization(opt => -{ - opt.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); -}); - builder.Services.AddSingleton((path, c) => { DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(path, c); @@ -78,100 +65,28 @@ app.UseDefaultFiles(); app.UseStaticFiles(); -app.Use(async (c, n) => -{ - //if (c.Request.Path.ToString().EndsWith("/openapi/v1.json")) - //{ - // c.Request.Headers"X-CSRF", "1"); - //} - await n(); -}); - app.UseAuthentication(); app.UseBff(); app.MapBffManagementEndpoints(); +// proxy all api's. app.MapRemoteBffApiEndpoint("/api1", Services.Api1.LogicalUri().ToString()) .WithOptionalUserAccessToken(); app.MapRemoteBffApiEndpoint("/api2", Services.Api2.LogicalUri().ToString()) .WithOptionalUserAccessToken(); -app.UseAuthorization(); app.UseSwaggerUI(c => { - + // Inject a javascript function to add a CSRF header to all requests c.UseRequestInterceptor("function(request){ request.headers['X-CSRF'] = '1';return request;}"); + // Add some javascript that adds a login / logout button to the page. c.InjectJavascript("bff-auth-button.js"); + + // Add all swagger endpoints for all APIs c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); c.SwaggerEndpoint("/api2/openapi/v1.json", "Api2"); }); app.Run(); - -public class OpenApiResponseTransform(string basePath) : ResponseTransform -{ - public override async ValueTask ApplyAsync(ResponseTransformContext context) - { - // Check if the request path matches /openapi/{document}.json - if (context.HttpContext.Request.Path.StartsWithSegments(basePath +"/openapi", out var remainingPath) && - remainingPath.HasValue && remainingPath.Value.EndsWith(".json")) - { - var readAsStreamAsync = await context.ProxyResponse.Content.ReadAsStreamAsync(); - var doc = new OpenApiStreamReader().Read(readAsStreamAsync, out var diagnostic); - context.SuppressResponseBody = true; - - - doc.Servers.Clear(); - doc.Servers.Add(new OpenApiServer() - { - Url = new Uri(Services.Bff.ActualUri(), basePath).ToString() - }); - - // remove the jwt security scheme - doc.Components.SecuritySchemes.Clear(); - // Add the cookie security scheme - //doc.Components.SecuritySchemes.Add("cookieAuth", new OpenApiSecurityScheme - //{ - // Type = SecuritySchemeType.Http, - // In = ParameterLocation.Cookie, - // Name = "__Host-bff", - // Description = "Cookie-based authentication" - //}); - - foreach (var path in doc.Paths) - { - foreach (var operation in path.Value.Operations) - { - //operation.Value.Responses.Add("401", new OpenApiResponse() - //{ - // Description = "Unauthorized" - //}); - //operation.Value.Parameters.Add(new OpenApiParameter() - //{ - // In = ParameterLocation.Header, - // Name = "X-CSRF", - // Required = true, - // Schema = new OpenApiSchema() - // { - // Type = "string", - // Default = new OpenApiString("1") - // } - - //}); - } - } - // Read and parse the existing JSON content - - var memoryStream = new MemoryStream(); - doc.Serialize(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); - memoryStream.Position = 0; - await memoryStream.CopyToAsync(context.HttpContext.Response.Body); - await context.HttpContext.Response.Body.FlushAsync(); - - } - } -} - - From 5179a3eeb63824b8762b7018c95e55957c5e4140 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 15:01:02 +0100 Subject: [PATCH 10/15] add button to swaggerui on home page --- BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js | 4 +-- BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js index cde9cc3c..8c59762a 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/app.js @@ -1,7 +1,7 @@ -const loginUrl = "/bff/login"; +const loginUrl = "/bff/login"; const silentLoginUrl = "/bff/silent-login"; const userUrl = "/bff/user"; -const apiUrl = "/api"; +const apiUrl = "/api1/weatherforecastFahrenheit"; let logoutUrl = "/bff/logout"; async function onLoad() { diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html index 1379544d..0f5652c2 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -13,18 +13,19 @@

Hello BFF

-
    -
  • Home
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
+
From 40db3747aa622311eec044d7796537683527080c Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 21 Mar 2025 15:56:08 +0100 Subject: [PATCH 11/15] wip --- .../OpenApi/OpenApi.Api1/OpenApi.Api1.csproj | 7 +- BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.json | 70 +++++++++++++++++ .../OpenApi/OpenApi.Api2/OpenApi.Api2.csproj | 7 +- BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.json | 70 +++++++++++++++++ BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj | 4 +- .../OpenApi.Bff/OpenApiResponseTransform.cs | 48 ++---------- .../OpenApi.BffOpenApiDocumentParser.csproj | 14 ++++ .../OpenApiTransformer.cs | 57 ++++++++++++++ .../Program.cs | 78 +++++++++++++++++++ BFF/v3/OpenApi/OpenApi.sln | 14 ++++ 10 files changed, 323 insertions(+), 46 deletions(-) create mode 100644 BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.json create mode 100644 BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.json create mode 100644 BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj create mode 100644 BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs create mode 100644 BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs diff --git a/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj index b9dd54b5..2439e146 100644 --- a/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj +++ b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.csproj @@ -1,13 +1,18 @@ - + net9.0 enable enable + . + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.json b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.json new file mode 100644 index 00000000..4933f59e --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api1/OpenApi.Api1.json @@ -0,0 +1,70 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApi.Api1 | v1", + "version": "1.0.0" + }, + "paths": { + "/weatherforecastFahrenheit": { + "get": { + "tags": [ + "OpenApi.Api1" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "WeatherForecast": { + "required": [ + "date", + "temperatureF", + "summary" + ], + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "temperatureF": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string", + "nullable": true + } + } + } + }, + "securitySchemes": { + "Bearer": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "Json Web Token" + } + } + }, + "tags": [ + { + "name": "OpenApi.Api1" + } + ] +} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj index 28d03f7f..8ece4d99 100644 --- a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj +++ b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.csproj @@ -1,9 +1,10 @@ - + net9.0 enable enable + . @@ -11,6 +12,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.json b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.json new file mode 100644 index 00000000..fa561c38 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Api2/OpenApi.Api2.json @@ -0,0 +1,70 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApi.Api2 | v1", + "version": "1.0.0" + }, + "paths": { + "/WeatherforecastCelcius": { + "get": { + "tags": [ + "OpenApi.Api2" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherForecast" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "WeatherForecast": { + "required": [ + "date", + "temperatureC", + "summary" + ], + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string", + "nullable": true + } + } + } + }, + "securitySchemes": { + "Bearer": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "Json Web Token" + } + } + }, + "tags": [ + { + "name": "OpenApi.Api2" + } + ] +} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj index 34b90753..b23239b3 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi.Bff.csproj @@ -9,13 +9,15 @@ - + + + diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs b/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs index 620600b6..5d008253 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs @@ -1,21 +1,18 @@ // Copyright (c) Duende Software. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using Microsoft.OpenApi; -using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Readers; +using OpenApi.BffOpenApiDocumentParser; using Yarp.ReverseProxy.Transforms; namespace OpenApi.Bff; /// -/// Transform the openapi document as it's being streamed. +/// TransformOpenApiDocumentForBff the openapi document as it's being streamed. /// /// public class OpenApiResponseTransform(string basePath) : ResponseTransform { - private static readonly OpenApiStreamReader OpenApiStreamReader = new OpenApiStreamReader(); public override async ValueTask ApplyAsync(ResponseTransformContext context) { @@ -25,49 +22,14 @@ public override async ValueTask ApplyAsync(ResponseTransformContext context) if (context.ProxyResponse == null) // nothing to do if no response from the proxy return; - - var openApiDocumentStream = await context.ProxyResponse.Content.ReadAsStreamAsync(); - var doc = OpenApiStreamReader.Read(openApiDocumentStream, out var diagnostic); - - // This line is needed because we're going to modify the output stream. - // If we don't do this, it's going to send both the original and the modified stream. + var outputStream = context.HttpContext.Response.Body; context.SuppressResponseBody = true; - // Make sure the server is actually the BFF, not the original urls. - // All traffic is supposed to go through the bff. - doc.Servers.Clear(); - doc.Servers.Add(new OpenApiServer() - { - //Url = new Uri(Services.Bff.ActualUri(), basePath).ToString() - Url = Services.Bff.ActualUri().ToString() - }); - - // We remove the JWT security scheme, because the BFF changes the way - // the auth works to Cookie only. Specifying the cookie in the openapi document - // isn't useful. - doc.Components.SecuritySchemes.Clear(); - - // Combine the pathbase with the paths in the document, so that the - // paths are correct for the proxy. - var allPaths = doc.Paths.ToArray(); - doc.Paths.Clear(); - foreach (var path in allPaths) - { - doc.Paths[basePath + path.Key] = path.Value; - } - - await WriteDocumentToResponseStream(context, doc); + var openApiDocumentStream = await context.ProxyResponse.Content.ReadAsStreamAsync(); + await OpenApiTransformer.TransformOpenApiDocumentForBff(openApiDocumentStream, outputStream, Services.Bff.ActualUri(), basePath); } } - private static async Task WriteDocumentToResponseStream(ResponseTransformContext context, OpenApiDocument doc) - { - var memoryStream = new MemoryStream(); - doc.Serialize(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); - memoryStream.Position = 0; - await memoryStream.CopyToAsync(context.HttpContext.Response.Body); - await context.HttpContext.Response.Body.FlushAsync(); - } private void AddPathBaseToAllPaths(OpenApiDocument doc) { diff --git a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj new file mode 100644 index 00000000..05168961 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj @@ -0,0 +1,14 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + diff --git a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs new file mode 100644 index 00000000..97c30a03 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs @@ -0,0 +1,57 @@ +using Microsoft.OpenApi; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace OpenApi.BffOpenApiDocumentParser; + +public class OpenApiTransformer +{ + private static readonly OpenApiStreamReader OpenApiStreamReader = new OpenApiStreamReader(); + + public static async Task TransformOpenApiDocumentForBff(Stream openApiDocumentStream, Stream outputStream, Uri serverUri, string localPath) + { + var doc = OpenApiStreamReader.Read(openApiDocumentStream, out var diagnostic); + + // This line is needed because we're going to modify the output stream. + // If we don't do this, it's going to send both the original and the modified stream. + + // Make sure the server is actually the BFF, not the original urls. + // All traffic is supposed to go through the bff. + doc.Servers.Clear(); + doc.Servers.Add(new OpenApiServer() + { + //Url = new Uri(Services.Bff.ActualUri(), basePath).ToString() + Url = serverUri.ToString() + }); + + // We remove the JWT security scheme, because the BFF changes the way + // the auth works to Cookie only. Specifying the cookie in the openapi document + // isn't useful. + doc.Components.SecuritySchemes.Clear(); + + // Combine the pathbase with the paths in the document, so that the + // paths are correct for the proxy. + var allPaths = doc.Paths.ToArray(); + doc.Paths.Clear(); + foreach (var path in allPaths) + { + doc.Paths[localPath + path.Key] = path.Value; + } + + + await WriteDocumentTo(doc, outputStream); + } + + + + private static async Task WriteDocumentTo(OpenApiDocument doc, Stream responseBody) + { + var memoryStream = new MemoryStream(); + doc.Serialize(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); + memoryStream.Position = 0; + await memoryStream.CopyToAsync(responseBody); + await responseBody.FlushAsync(); + } + +} diff --git a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs new file mode 100644 index 00000000..5172ad2f --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs @@ -0,0 +1,78 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using OpenApi.BffOpenApiDocumentParser; + +class Program +{ + static void Main(string[] args) + { + var rootCommand = new RootCommand + { + new Option( + aliases: ["-i", "--input-file"], + description: "The input file to be processed") + { + Required = true + }, + new Option( + aliases: ["-a", "--api-path"], + description: "The API path to be used during transformation") + { + Required = true + }, + new Option( + aliases: ["-s", "--serverUrl"], + description: "The server URL to be used during transformation") + { + Required = true + }, + new Option( + aliases: ["-o", "--output-path"], + description: "The output directory where the modified file will be saved") + { + Required = true + } + }; + + rootCommand.Description = "OpenApi.BffOpenApiDocumentParser"; + + rootCommand.Handler = CommandHandler.Create(async (infile, apiPath, serverUrl, outputpath) => + { + //if (string.IsNullOrEmpty(infile) || string.IsNullOrEmpty(apiPath) || string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(outputpath)) + //{ + // Console.WriteLine("Usage: OpenApi.BffOpenApiDocumentParser -i --apiPath --serverUrl --outputpath "); + // return; + //} + + if (!File.Exists(infile)) + { + Console.WriteLine($"File not found: {infile}"); + return; + } + + if (!Directory.Exists(outputpath)) + { + Directory.CreateDirectory(outputpath); + } + + string destFile = Path.Combine(outputpath, Path.GetFileName(infile)); + await ModifyAndCopyFile(infile, apiPath, serverUrl, destFile); + }); + + rootCommand.Invoke(args); + } + + static async Task ModifyAndCopyFile(string sourceFile, string apiPath, string serverUrl, string destFile) + { + // Read the file content + var input = File.OpenRead(sourceFile); + + // Write the modified content to the destination file + using var output = File.OpenWrite(destFile); + + await OpenApiTransformer.TransformOpenApiDocumentForBff(input, output, new Uri(serverUrl), apiPath); + } + +} diff --git a/BFF/v3/OpenApi/OpenApi.sln b/BFF/v3/OpenApi/OpenApi.sln index 2456c1af..2e5334d6 100644 --- a/BFF/v3/OpenApi/OpenApi.sln +++ b/BFF/v3/OpenApi/OpenApi.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.Api2", "OpenApi.Api EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.Bff", "OpenApi.Bff\OpenApi.Bff.csproj", "{2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApi.BffOpenApiDocumentParser", "OpenApi.BffOpenApiDocumentParser\OpenApi.BffOpenApiDocumentParser.csproj", "{EF16CDD2-C1E6-4BC5-A020-58E45551A634}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,18 @@ Global {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|x64.Build.0 = Release|Any CPU {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|x86.ActiveCfg = Release|Any CPU {2D913F15-29E4-41A6-8CC7-20FB3BCD88E3}.Release|x86.Build.0 = Release|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Debug|x64.Build.0 = Debug|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Debug|x86.Build.0 = Debug|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Release|Any CPU.Build.0 = Release|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Release|x64.ActiveCfg = Release|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Release|x64.Build.0 = Release|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Release|x86.ActiveCfg = Release|Any CPU + {EF16CDD2-C1E6-4BC5-A020-58E45551A634}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 535633fab84a146a45bc2a8ef6b71ba8d710ac3d Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Mon, 24 Mar 2025 09:25:59 +0100 Subject: [PATCH 12/15] cleaned up --- .../OpenApi/OpenApiDocumentCombiner.cs | 93 +++++++++++++++++++ .../OpenApiDocumentCombinerExtensions.cs | 11 +++ .../OpenApi/OpenApiDocumentCombinerOptions.cs | 8 ++ .../OpenApi/OpenApiDocumentSource.cs | 3 + .../OpenApi.Bff/OpenApiResponseTransform.cs | 17 +--- BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 37 ++++++-- .../OpenApi.BffOpenApiDocumentParser.csproj | 2 +- .../OpenApiTransformer.cs | 4 - .../Program.cs | 14 +-- .../Properties/launchSettings.json | 9 ++ 10 files changed, 165 insertions(+), 33 deletions(-) create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombiner.cs create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerExtensions.cs create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerOptions.cs create mode 100644 BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentSource.cs create mode 100644 BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Properties/launchSettings.json diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombiner.cs b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombiner.cs new file mode 100644 index 00000000..8ce163fb --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombiner.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace OpenApi.Bff.OpenApi +{ + public class OpenApiDocumentCombiner(HttpClient client, IOptions o) + { + private static readonly OpenApiStreamReader OpenApiStreamReader = new OpenApiStreamReader(); + + public async Task CombineDocuments(CancellationToken cancellationToken) + { + OpenApiDocument doc = new OpenApiDocument(); + + if (o.Value.ServerUri != null) + { + doc.Servers.Add(new OpenApiServer() + { + Url = o.Value.ServerUri.ToString() + }); + } + + doc.Paths = new OpenApiPaths(); + doc.Components = new OpenApiComponents(); + + foreach (var source in o.Value.Documents) + { + var stream = await client.GetStreamAsync(source.DocumentUri, cancellationToken); + var docToMerge = OpenApiStreamReader.Read(stream, out var diagnostic); + + foreach (var path in docToMerge.Paths ?? []) + { + doc.Paths[source.LocalPath + path.Key] = path.Value; + } + + foreach (var schema in docToMerge.Components.Schemas) + { + doc.Components.Schemas[schema.Key] = schema.Value; + } + foreach (var response in docToMerge.Components.Responses) + { + doc.Components.Responses[response.Key] = response.Value; + } + + foreach (var parameter in docToMerge.Components.Parameters) + { + doc.Components.Parameters[parameter.Key] = parameter.Value; + } + + foreach (var example in docToMerge.Components.Examples) + { + doc.Components.Examples[example.Key] = example.Value; + } + + foreach (var requestBody in docToMerge.Components.RequestBodies) + { + doc.Components.RequestBodies[requestBody.Key] = requestBody.Value; + } + + foreach (var header in docToMerge.Components.Headers) + { + doc.Components.Headers[header.Key] = header.Value; + } + + //// We intentionally don't copy the security schemes. + //foreach (var securityScheme in docToMerge.Components.SecuritySchemes) + //{ + // doc.Components.SecuritySchemes[securityScheme.Key] = securityScheme.Value; + //} + + foreach (var link in docToMerge.Components.Links) + { + doc.Components.Links[link.Key] = link.Value; + } + + foreach (var callback in docToMerge.Components.Callbacks) + { + doc.Components.Callbacks[callback.Key] = callback.Value; + } + + } + + var memoryStream = new MemoryStream(); + doc.Serialize(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json); + memoryStream.Position = 0; + + return TypedResults.Stream(memoryStream); + } + } +} diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerExtensions.cs b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerExtensions.cs new file mode 100644 index 00000000..18d29598 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerExtensions.cs @@ -0,0 +1,11 @@ +namespace OpenApi.Bff.OpenApi; + +public static class OpenApiDocumentCombinerExtensions +{ + public static IServiceCollection AddOpenApiDocumentsCombiner(this IServiceCollection services, Action options) + { + services.Configure(options); + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerOptions.cs b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerOptions.cs new file mode 100644 index 00000000..4070a33e --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentCombinerOptions.cs @@ -0,0 +1,8 @@ +namespace OpenApi.Bff.OpenApi; + +public class OpenApiDocumentCombinerOptions +{ + public Uri? ServerUri { get; set; } + + public OpenApiDocumentSource[] Documents { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentSource.cs b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentSource.cs new file mode 100644 index 00000000..47c44d92 --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApi/OpenApiDocumentSource.cs @@ -0,0 +1,3 @@ +namespace OpenApi.Bff.OpenApi; + +public record OpenApiDocumentSource(string LocalPath, Uri DocumentUri); \ No newline at end of file diff --git a/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs b/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs index 5d008253..6ba1c5fd 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/OpenApiResponseTransform.cs @@ -23,6 +23,9 @@ public override async ValueTask ApplyAsync(ResponseTransformContext context) // nothing to do if no response from the proxy return; var outputStream = context.HttpContext.Response.Body; + + // This line is needed because we're going to modify the output stream. + // If we don't do this, it's going to send both the original and the modified stream. context.SuppressResponseBody = true; var openApiDocumentStream = await context.ProxyResponse.Content.ReadAsStreamAsync(); @@ -30,19 +33,7 @@ public override async ValueTask ApplyAsync(ResponseTransformContext context) } } - - private void AddPathBaseToAllPaths(OpenApiDocument doc) - { - var pathClones = doc.Paths.ToArray(); - doc.Paths.Clear(); - - foreach (var path in pathClones) - { - doc.Paths[basePath + path.Key] = path.Value; - } - } - - private bool ProxyingOpenApiDocument(ResponseTransformContext context) +private bool ProxyingOpenApiDocument(ResponseTransformContext context) { return context.HttpContext.Request.Path.StartsWithSegments(basePath +"/openapi", out var remainingPath) && remainingPath.HasValue && (remainingPath.Value.EndsWith(".json") || remainingPath.Value.EndsWith(".yaml")); diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index f4aec1a4..9056a452 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -1,6 +1,12 @@ -using OpenApi.Bff; using Duende.Bff.Yarp; -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using OpenApi.Bff; +using OpenApi.Bff.OpenApi; var builder = WebApplication.CreateBuilder(args); @@ -12,15 +18,26 @@ // Make sure Yarp understands aspire's service discovery. builder.Services.AddHttpForwarderWithServiceDiscovery(); +builder.Services.AddOpenApiDocumentsCombiner(opt => +{ + opt.ServerUri = Services.Bff.ActualUri(); + opt.Documents = new[] + { + new OpenApiDocumentSource("/api1", new Uri(Services.Api1.LogicalUri(), "/openapi/v1.json")), + new OpenApiDocumentSource("/api2", new Uri(Services.Api2.LogicalUri(), "/openapi/v1.json")), + }; +}); + + Configuration config = new(); builder.Configuration.Bind("BFF", config); builder.Services.AddAuthentication(options => - { - options.DefaultScheme = "cookie"; - options.DefaultChallengeScheme = "oidc"; - options.DefaultSignOutScheme = "oidc"; - }) +{ + options.DefaultScheme = "cookie"; + options.DefaultChallengeScheme = "oidc"; + options.DefaultSignOutScheme = "oidc"; +}) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-bff"; @@ -76,6 +93,9 @@ app.MapRemoteBffApiEndpoint("/api2", Services.Api2.LogicalUri().ToString()) .WithOptionalUserAccessToken(); +app.MapGet("/swagger/combined/v1.json", + async (OpenApiDocumentCombiner c, CancellationToken ct) => await c.CombineDocuments(ct)); + app.UseSwaggerUI(c => { // Inject a javascript function to add a CSRF header to all requests @@ -86,7 +106,8 @@ // Add all swagger endpoints for all APIs c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); - c.SwaggerEndpoint("/api2/openapi/v1.json", "Api2"); + c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); + c.SwaggerEndpoint("/swagger/combined/v1.json", "Combined"); }); app.Run(); diff --git a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj index 05168961..4b3285c3 100644 --- a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj +++ b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApi.BffOpenApiDocumentParser.csproj @@ -7,7 +7,7 @@ enable - + diff --git a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs index 97c30a03..12e42e5e 100644 --- a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs +++ b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/OpenApiTransformer.cs @@ -13,15 +13,11 @@ public static async Task TransformOpenApiDocumentForBff(Stream openApiDocumentSt { var doc = OpenApiStreamReader.Read(openApiDocumentStream, out var diagnostic); - // This line is needed because we're going to modify the output stream. - // If we don't do this, it's going to send both the original and the modified stream. - // Make sure the server is actually the BFF, not the original urls. // All traffic is supposed to go through the bff. doc.Servers.Clear(); doc.Servers.Add(new OpenApiServer() { - //Url = new Uri(Services.Bff.ActualUri(), basePath).ToString() Url = serverUri.ToString() }); diff --git a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs index 5172ad2f..2ac498db 100644 --- a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Program.cs @@ -38,7 +38,7 @@ static void Main(string[] args) rootCommand.Description = "OpenApi.BffOpenApiDocumentParser"; - rootCommand.Handler = CommandHandler.Create(async (infile, apiPath, serverUrl, outputpath) => + rootCommand.Handler = CommandHandler.Create(async (inputFile, apiPath, serverUrl, outputPath) => { //if (string.IsNullOrEmpty(infile) || string.IsNullOrEmpty(apiPath) || string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(outputpath)) //{ @@ -46,19 +46,19 @@ static void Main(string[] args) // return; //} - if (!File.Exists(infile)) + if (!File.Exists(inputFile)) { - Console.WriteLine($"File not found: {infile}"); + Console.WriteLine($"File not found: {inputFile}"); return; } - if (!Directory.Exists(outputpath)) + if (!Directory.Exists(outputPath)) { - Directory.CreateDirectory(outputpath); + Directory.CreateDirectory(outputPath); } - string destFile = Path.Combine(outputpath, Path.GetFileName(infile)); - await ModifyAndCopyFile(infile, apiPath, serverUrl, destFile); + string destFile = Path.Combine(outputPath, Path.GetFileName(inputFile)); + await ModifyAndCopyFile(inputFile, apiPath, serverUrl, destFile); }); rootCommand.Invoke(args); diff --git a/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Properties/launchSettings.json new file mode 100644 index 00000000..97d77dda --- /dev/null +++ b/BFF/v3/OpenApi/OpenApi.BffOpenApiDocumentParser/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "OpenApi.BffOpenApiDocumentParser": { + "commandName": "Project", + //"commandLineArgs": "-i ../OpenApi.Api1/OpenApi.Api1.json -s https://server -a /api1 -o d:/temp", + //"workingDirectory": "$(projectdir)" + } + } +} From e1a9b08fbc7515a333320bbf133cdb266acceb3a Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Mon, 24 Mar 2025 09:28:13 +0100 Subject: [PATCH 13/15] remove http profiles --- .../OpenApi.Api1/Properties/launchSettings.json | 11 +---------- .../OpenApi.Api2/Properties/launchSettings.json | 11 +---------- .../OpenApi.Bff/Properties/launchSettings.json | 2 +- .../Properties/launchSettings.json | 12 ------------ 4 files changed, 3 insertions(+), 33 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json index 990d7d23..5ae1eded 100644 --- a/BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json +++ b/BFF/v3/OpenApi/OpenApi.Api1/Properties/launchSettings.json @@ -1,15 +1,6 @@ -{ +{ "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5265", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "https": { "commandName": "Project", "dotnetRunMessages": true, diff --git a/BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json index d05de986..12ab297e 100644 --- a/BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json +++ b/BFF/v3/OpenApi/OpenApi.Api2/Properties/launchSettings.json @@ -1,15 +1,6 @@ -{ +{ "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5084", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "https": { "commandName": "Project", "dotnetRunMessages": true, diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json index a81c40d0..80b483dc 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json +++ b/BFF/v3/OpenApi/OpenApi.Bff/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7082;http://localhost:5113", + "applicationUrl": "https://localhost:7082", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json index 8b728174..93bf54b2 100644 --- a/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json +++ b/BFF/v3/OpenApi/OpenApi.DevServer.AppHost/Properties/launchSettings.json @@ -12,18 +12,6 @@ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21026", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22140" } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15267", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19227", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20119" - } } } } From 4b1ad00e80607ecf3af66a9a3a4706d23c94c26d Mon Sep 17 00:00:00 2001 From: khalidabuhakmeh Date: Mon, 24 Mar 2025 08:45:06 -0400 Subject: [PATCH 14/15] Fix issue with SwaggerEndpoint registering API 1 twice. --- BFF/v3/OpenApi/OpenApi.Bff/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs index 9056a452..4f5b2b9f 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/Program.cs +++ b/BFF/v3/OpenApi/OpenApi.Bff/Program.cs @@ -105,8 +105,8 @@ c.InjectJavascript("bff-auth-button.js"); // Add all swagger endpoints for all APIs - c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); - c.SwaggerEndpoint("/api1/openapi/v1.json", "Api1"); + c.SwaggerEndpoint("/api1/openapi/v1.json", "API #1"); + c.SwaggerEndpoint("/api2/openapi/v1.json", "API #2"); c.SwaggerEndpoint("/swagger/combined/v1.json", "Combined"); }); From 996405d3d9c5e53b692e3bab5a9da6917581dd4a Mon Sep 17 00:00:00 2001 From: khalidabuhakmeh Date: Mon, 24 Mar 2025 09:25:35 -0400 Subject: [PATCH 15/15] Improve authentication button handling in Swagger UI Refactor logic to dynamically add authentication buttons only when not already present. Introduce a more reliable method to detect and handle the rendering of the Swagger UI elements using MutationObserver with additional safeguards. This ensures improved performance and stability in handling authentication states. --- .../wwwroot/swagger/bff-auth-button.js | 86 +++++++++++-------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js index 2de13b00..599ff79f 100644 --- a/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js +++ b/BFF/v3/OpenApi/OpenApi.Bff/wwwroot/swagger/bff-auth-button.js @@ -1,52 +1,68 @@ async function checkAuthentication() { try { - let response = await fetch('/bff/user', { credentials: 'include', headers: { "x-csrf": 1 } }); - - let wrapper = document.querySelector('.schemes.wrapper'); - - wrapper.insertAdjacentHTML('beforeend', `
`); - + // check if we already added it let authorizeButton = document.querySelector('.swagger-ui .auth-wrapper .btn'); - if (!authorizeButton) return; - - if (response.status === 401) { - // User is unauthenticated - authorizeButton.innerText = "Log in"; - authorizeButton.onclick = function (event) { - event.preventDefault(); - window.location.href = "/bff/login?returnUrl=" + window.location.pathname; - }; + if (authorizeButton === null) { - } else if (response.status === 200) { + let response = await fetch('/bff/user', + { + credentials: 'include', + headers: {'x-csrf': '1'} + }); - let claims = await response.json(); + let wrapper = document.querySelector('.schemes.wrapper'); + wrapper.insertAdjacentHTML('beforeend', `
`); + authorizeButton = document.querySelector('.swagger-ui .auth-wrapper .btn'); - let logoutUrlClaim = claims.find(claim => claim.type === 'bff:logout_url'); - if (logoutUrlClaim) { + if (response.status === 401) { + // User is unauthenticated + authorizeButton.innerText = "Log in"; authorizeButton.onclick = function (event) { event.preventDefault(); - window.location.href = `${logoutUrlClaim.value}&returnUrl=${window.location.pathname}`; + window.location.href = "/bff/login?returnUrl=" + window.location.pathname; }; - } - // User is authenticated - authorizeButton.innerText = "Log out"; - authorizeButton.classList.add("btn-success"); // Optional: Change button style + } else if (response.status === 200) { + let claims = await response.json(); + let logoutUrlClaim = claims.find(claim => claim.type === 'bff:logout_url'); + if (logoutUrlClaim) { + authorizeButton.onclick = function (event) { + event.preventDefault(); + window.location.href = `${logoutUrlClaim.value}&returnUrl=${window.location.pathname}`; + }; + } + // User is authenticated + authorizeButton.innerText = "Log out"; + authorizeButton.classList.add("btn-success"); + } } - } catch (error) { + } catch + (error) { console.error("Error checking authentication:", error); } } -const observer = new MutationObserver((mutations, obs) => { - const wrapper = document.querySelector('.schemes.wrapper'); - if (wrapper) { - checkAuthentication(); - obs.disconnect(); - } -}); +document.addEventListener("DOMContentLoaded", e => { + // watch the only element that's on the page when the page loads + const swaggerUi = document.querySelector('#swagger-ui'); + + const observer = new MutationObserver(async (mutations, obs) => { + let schemeContainer = document.querySelector('.scheme-container'); + const intervalId = setInterval(() => { + schemeContainer = document.querySelector('.scheme-container'); + if (schemeContainer) { + clearInterval(intervalId); + checkAuthentication(); + } + }, 100); + }); -observer.observe(document, { - childList: true, - subtree: true + observer.observe(swaggerUi, { + childList: true, + subtree: true + }); }); + + + +