migrate bffs
This commit is contained in:
parent
1c5c0c11c4
commit
5b5b0d1b7d
@ -1,8 +1,8 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY scripts scripts/
|
||||
|
@ -12,5 +12,6 @@
|
||||
<PackageReference Include="Ocelot" Version="$(Ocelot)" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="$(Serilog_AspNetCore)" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="$(Serilog_Sinks_Console)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(Microsoft_AspNetCore_Authentication_JwtBearer)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,14 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using System.IO;
|
||||
|
||||
namespace OcelotApiGw
|
||||
{
|
||||
|
@ -1,14 +1,13 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using HealthChecks.UI.Client;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Ocelot.DependencyInjection;
|
||||
using Ocelot.Middleware;
|
||||
using System;
|
||||
using HealthChecks.UI.Client;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace OcelotApiGw
|
||||
{
|
||||
|
@ -1,8 +1,8 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY scripts scripts/
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters
|
||||
{
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -10,7 +10,7 @@
|
||||
{
|
||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(Operation operation, OperationFilterContext context)
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Check for authorize attribute
|
||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||
@ -18,14 +18,19 @@
|
||||
|
||||
if (!hasAuthorize) return;
|
||||
|
||||
operation.Responses.TryAdd("401", new Response { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new Response { Description = "Forbidden" });
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
operation.Security = new List<IDictionary<string, IEnumerable<string>>>
|
||||
var oAuthScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
new Dictionary<string, IEnumerable<string>>
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{ "oauth2", new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" } }
|
||||
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
<AssemblyName>Mobile.Shopping.HttpAggregator</AssemblyName>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator</RootNamespace>
|
||||
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -20,6 +22,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="$(Serilog_Sinks_Console)" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="$(Swashbuckle_AspNetCore)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="$(Microsoft_Extensions_Http_Polly)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(Microsoft_AspNetCore_Authentication_JwtBearer)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,12 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator
|
||||
|
@ -1,9 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http;
|
||||
using Devspaces.Support;
|
||||
using HealthChecks.UI.Client;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -13,14 +12,14 @@ using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
|
||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using HealthChecks.UI.Client;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Devspaces.Support;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator
|
||||
{
|
||||
@ -110,28 +109,31 @@ namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator
|
||||
services.Configure<UrlsConfig>(configuration.GetSection("urls"));
|
||||
|
||||
services.AddMvc()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
|
||||
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.DescribeAllEnumsAsStrings();
|
||||
options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
|
||||
options.SwaggerDoc("v1", new OpenApi.Models.OpenApiInfo
|
||||
{
|
||||
Title = "Shopping Aggregator for Mobile Clients",
|
||||
Version = "v1",
|
||||
Description = "Shopping Aggregator for Mobile Clients",
|
||||
TermsOfService = "Terms Of Service"
|
||||
Description = "Shopping Aggregator for Mobile Clients"
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("oauth2", new OAuth2Scheme
|
||||
options.AddSecurityDefinition("oauth2", new OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Type = "oauth2",
|
||||
Flow = "implicit",
|
||||
AuthorizationUrl = $"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize",
|
||||
TokenUrl = $"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token",
|
||||
Scopes = new Dictionary<string, string>()
|
||||
Flows = new OpenApi.Models.OpenApiOAuthFlows()
|
||||
{
|
||||
{ "mobileshoppingagg", "Shopping Aggregator for Mobile Clients" }
|
||||
Implicit = new OpenApi.Models.OpenApiOAuthFlow()
|
||||
{
|
||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
|
||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>()
|
||||
{
|
||||
{ "marketing", "Marketing API" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY scripts scripts/
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters
|
||||
{
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -10,7 +10,7 @@
|
||||
{
|
||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(Operation operation, OperationFilterContext context)
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Check for authorize attribute
|
||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||
@ -18,14 +18,19 @@
|
||||
|
||||
if (!hasAuthorize) return;
|
||||
|
||||
operation.Responses.TryAdd("401", new Response { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new Response { Description = "Forbidden" });
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
operation.Security = new List<IDictionary<string, IEnumerable<string>>>
|
||||
var oAuthScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
new Dictionary<string, IEnumerable<string>>
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{ "oauth2", new [] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" } }
|
||||
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator
|
||||
|
@ -1,5 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Devspaces.Support;
|
||||
using HealthChecks.UI.Client;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -9,19 +12,14 @@ using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
|
||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using Polly.Timeout;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http;
|
||||
using HealthChecks.UI.Client;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Devspaces.Support;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator
|
||||
{
|
||||
@ -137,28 +135,31 @@ namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator
|
||||
services.Configure<UrlsConfig>(configuration.GetSection("urls"));
|
||||
|
||||
services.AddMvc()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
|
||||
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.DescribeAllEnumsAsStrings();
|
||||
options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
|
||||
options.SwaggerDoc("v1", new OpenApi.Models.OpenApiInfo
|
||||
{
|
||||
Title = "Shopping Aggregator for Web Clients",
|
||||
Version = "v1",
|
||||
Description = "Shopping Aggregator for Web Clients",
|
||||
TermsOfService = "Terms Of Service"
|
||||
Description = "Shopping Aggregator for Web Clients"
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("oauth2", new OAuth2Scheme
|
||||
options.AddSecurityDefinition("oauth2", new OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Type = "oauth2",
|
||||
Flow = "implicit",
|
||||
AuthorizationUrl = $"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize",
|
||||
TokenUrl = $"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token",
|
||||
Scopes = new Dictionary<string, string>()
|
||||
Flows = new OpenApi.Models.OpenApiOAuthFlows()
|
||||
{
|
||||
{ "webshoppingagg", "Shopping Aggregator for Web Clients" }
|
||||
Implicit = new OpenApi.Models.OpenApiOAuthFlow()
|
||||
{
|
||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
|
||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>()
|
||||
{
|
||||
{ "webshoppingagg", "Shopping Aggregator for Web Clients" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,8 @@
|
||||
<AssemblyName>Web.Shopping.HttpAggregator</AssemblyName>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator</RootNamespace>
|
||||
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -19,6 +21,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="$(Serilog_Sinks_Console)" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="$(Swashbuckle_AspNetCore)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="$(Microsoft_Extensions_Http_Polly)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(Microsoft_AspNetCore_Authentication_JwtBearer)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -55,7 +55,7 @@
|
||||
{
|
||||
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
||||
})
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
|
||||
.AddControllersAsServices(); //Injecting Controllers themselves thru DIFor further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services
|
||||
|
||||
services.Configure<MarketingSettings>(Configuration);
|
||||
|
@ -1,16 +1,14 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ordering.API.Infrastructure.Filters
|
||||
{
|
||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(Operation operation, OperationFilterContext context)
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Check for authorize attribute
|
||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||
@ -18,16 +16,21 @@ namespace Ordering.API.Infrastructure.Filters
|
||||
|
||||
if (!hasAuthorize) return;
|
||||
|
||||
operation.Responses.TryAdd("401", new Response { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new Response { Description = "Forbidden" });
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
operation.Security = new List<IDictionary<string, IEnumerable<string>>>
|
||||
var oAuthScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
new Dictionary<string, IEnumerable<string>>
|
||||
{
|
||||
{ "oauth2", new [] { "orderingapi" } }
|
||||
}
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
[ oAuthScheme ] = new [] { "orderingapi" }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -155,7 +155,7 @@
|
||||
{
|
||||
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
||||
})
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
|
||||
.AddControllersAsServices(); //Injecting Controllers themselves thru DI
|
||||
//For further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user