Add project files.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace MidrandBooksApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
public sealed class ApiVersionTargetAttribute(int majorVersion) : Attribute
|
||||
{
|
||||
public int MajorVersion { get; } = majorVersion;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MidrandBooksApi;
|
||||
|
||||
public static class EndpointTags
|
||||
{
|
||||
public const string Books = nameof(Books);
|
||||
public const string Payments = nameof(Payments);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MidrandBooksApi;
|
||||
|
||||
public interface IEndpoint
|
||||
{
|
||||
void Map(IEndpointRouteBuilder builder);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>a257fd9f-4f39-471a-b333-d5d6b51bd058</UserSecretsId>
|
||||
<AssemblyOriginatorKeyFile>..\MidrandBooks.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Security (IODC)-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IdentityModel.AspNetCore" Version="4.3.0" />
|
||||
<PackageReference Include="IdentityModel.AspNetCore.OAuth2introspection" Version="6.2.0" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||
<PackageReference Include="IdentityModel" Version="6.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Health Checks -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- API Versioning -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AccessTokenClient.Extensions" Version="5.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Asp.Versioning.Http" Version="10.0.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />
|
||||
|
||||
<Using Include="Asp.Versioning" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- API Documentation -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.14.14" />
|
||||
|
||||
<Using Include="Scalar.AspNetCore" />
|
||||
<Using Include="Microsoft.OpenApi" />
|
||||
<Using Include="Microsoft.AspNetCore.OpenApi" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- file nesting -->
|
||||
<ItemGroup>
|
||||
<ProjectCapability Include="ConfigurableFileNesting" />
|
||||
<ProjectCapability Include="ConfigurableFileNestingFeatureEnabled" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LiteCharms.Features" Version="1.61.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- UI -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ANM.Blazored.Toast" Version="0.1.1" />
|
||||
<PackageReference Include="LiteCharms.Features.MidrandBooks" Version="1.61.0" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="Blazored.Toast.Services" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- CQRS -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="FluentResults" />
|
||||
<Using Include="Mediator" />
|
||||
<Using Include="Quartz" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Health Checks -->
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Shared Global Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="System.Reflection" />
|
||||
<Using Include="Microsoft.Extensions.DependencyInjection.Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace MidrandBooksApi;
|
||||
|
||||
public sealed class OpenApiBearerSecuritySchemeTransformer : IOpenApiDocumentTransformer
|
||||
{
|
||||
public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var bearerScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\""
|
||||
};
|
||||
|
||||
document.AddComponent("Bearer", bearerScheme);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.Hasher;
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace MidrandBooksApi.Payments.Endpoints;
|
||||
|
||||
[ApiVersionTarget(1)]
|
||||
public sealed class ConfirmationEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapPost("payments/confirm", async (HttpRequest request, HashService hashService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var formCollection = await request.ReadFormAsync(cancellationToken);
|
||||
|
||||
if (!formCollection.TryGetValue("signature", out var signatureValues) || string.IsNullOrWhiteSpace(signatureValues.ToString()))
|
||||
return Results.BadRequest("Missing Payfast validation signature.");
|
||||
|
||||
string incomingSignature = signatureValues.ToString();
|
||||
|
||||
var payload = new PayfastWebhookPayload
|
||||
{
|
||||
Amount = formCollection.TryGetValue("amount", out var amountValues) ? amountValues.ToString() : null,
|
||||
ItemName = formCollection.TryGetValue("item_name", out var itemValues) ? itemValues.ToString() : null,
|
||||
MPaymentId = formCollection.TryGetValue("m_payment_id", out var paymentIdValues) ? paymentIdValues.ToString() : null
|
||||
};
|
||||
|
||||
var validationResult = hashService.VerifyPayfastWebhookSignature(payload, incomingSignature);
|
||||
|
||||
return validationResult.IsFailed || !validationResult.Value
|
||||
? Results.Unauthorized()
|
||||
: Results.Ok();
|
||||
})
|
||||
.WithDescription("Securely confirm and process an incoming Payfast merchant payment callback.")
|
||||
.WithName(typeof(ConfirmationEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Payments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Asp.Versioning.Builder;
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.Mediator;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using MidrandBooksApi;
|
||||
using static LiteCharms.Features.Extensions.Quartz;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddMonitoring();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
|
||||
builder.Services.AddApiServices(builder.Configuration);
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddAuthentication();
|
||||
|
||||
builder.Services.AddMediator();
|
||||
|
||||
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>));
|
||||
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>));
|
||||
|
||||
builder.Services.AddQuartzSchedulerClient(MidrandShopSchedulerName, builder.Configuration);
|
||||
|
||||
builder.Services.AddEmailServices(builder.Configuration);
|
||||
builder.Services.AddEmailServiceBus();
|
||||
|
||||
builder.Services.AddShopServices();
|
||||
builder.Services.AddHashServices(builder.Configuration);
|
||||
builder.Services.AddMidrandShopDatabase(builder.Configuration);
|
||||
|
||||
builder.Services.AddMidrandShopPostgresHealthCheck();
|
||||
builder.Services.AddMidrandShopQuartzHealthCheck();
|
||||
builder.Services.AddHealthChecksSupport(builder.Configuration);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
|
||||
var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName);
|
||||
|
||||
if (!scheduler!.IsStarted)
|
||||
await scheduler.Start();
|
||||
|
||||
app.UseHsts();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
ApiVersionSet versionSet = app.NewApiVersionSet("v1")
|
||||
.HasApiVersion(new ApiVersion(1))
|
||||
.HasApiVersion(new ApiVersion(2))
|
||||
.ReportApiVersions()
|
||||
.Build();
|
||||
|
||||
var versionGroups = new Dictionary<int, RouteGroupBuilder>
|
||||
{
|
||||
{ 1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet) }
|
||||
};
|
||||
|
||||
app.MapEndpoints(versionGroups);
|
||||
|
||||
app.UseHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
Predicate = _ => true,
|
||||
AllowCachingResponses = true,
|
||||
ResponseWriter = HealthChecks.UI.Client.UIResponseWriter.WriteHealthCheckUIResponse
|
||||
});
|
||||
|
||||
app.MapHealthChecksUI(options => { options.UIPath = "/healthui"; });
|
||||
app.UseHealthChecks("/ready");
|
||||
|
||||
app.MapOpenApi();
|
||||
|
||||
foreach (var description in app.DescribeApiVersions().OrderByDescending(o => o.ApiVersion.MajorVersion))
|
||||
app.MapScalarApiReference($"/openapi/{description.GroupName}", (options, context) =>
|
||||
{
|
||||
options.AddServer(new ScalarServer($"https://{context.Request.Host}"));
|
||||
options.WithOpenApiRoutePattern($"/openapi/{description.GroupName}.json");
|
||||
options.WithTheme(ScalarTheme.DeepSpace);
|
||||
options.Agent = new ScalarAgentOptions { Disabled = true };
|
||||
options.Authentication = new ScalarAuthenticationOptions { PreferredSecuritySchemes = ["Bearer"] };
|
||||
});
|
||||
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5159",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7196;http://localhost:5159",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
namespace MidrandBooksApi;
|
||||
|
||||
public static class Setup
|
||||
{
|
||||
public static IApplicationBuilder MapEndpoints(this WebApplication app, Dictionary<int, RouteGroupBuilder> versionGroups)
|
||||
{
|
||||
var endpoints = app.Services.GetRequiredService<IEnumerable<IEndpoint>>();
|
||||
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
var versionAttributes = endpoint.GetType().GetCustomAttributes<ApiVersionTargetAttribute>().ToList();
|
||||
|
||||
if (versionAttributes.Count != 0)
|
||||
{
|
||||
foreach (var attr in versionAttributes)
|
||||
if (versionGroups.TryGetValue(attr.MajorVersion, out var targetGroup))
|
||||
endpoint.Map(targetGroup);
|
||||
}
|
||||
else
|
||||
endpoint.Map(app);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly)
|
||||
{
|
||||
ServiceDescriptor[] discriptors = [.. assembly.DefinedTypes
|
||||
.Where(t => t is { IsInterface: false, IsAbstract: false })
|
||||
.Where(t => t.IsAssignableTo(typeof(IEndpoint)))
|
||||
.Select(t => ServiceDescriptor.Transient(typeof(IEndpoint), t))];
|
||||
|
||||
services.TryAddEnumerable(discriptors);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static string ToEndpointName(this Type target, string? annotation = "") =>
|
||||
$"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower();
|
||||
|
||||
public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1);
|
||||
options.ReportApiVersions = true;
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),
|
||||
new QueryStringApiVersionReader("version"),
|
||||
new QueryStringApiVersionReader("version"),
|
||||
new MediaTypeApiVersionReader("version"));
|
||||
})
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
|
||||
var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"];
|
||||
var healthUrl = "http://localhost:8080/health";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(urls))
|
||||
{
|
||||
string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))!
|
||||
.Replace("*", "localhost").Replace("+", "localhost");
|
||||
|
||||
healthUrl = $"{firstUrl.TrimEnd('/')}/health";
|
||||
}
|
||||
|
||||
services.AddHealthChecksUI(setup =>
|
||||
{
|
||||
setup.SetNotifyUnHealthyOneTimeUntilChange();
|
||||
setup.AddHealthCheckEndpoint("primary, heal", healthUrl);
|
||||
setup.SetHeaderText("Midrand Books");
|
||||
})
|
||||
.AddInMemoryStorage();
|
||||
|
||||
services.AddOutputCache(options =>
|
||||
{
|
||||
options.AddBasePolicy(builder => builder.Cache());
|
||||
options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
|
||||
services.AddOpenApi(options => options.AddDocumentTransformer<OpenApiBearerSecuritySchemeTransformer>());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"HasherSettings": {
|
||||
"MinHashLength": 11
|
||||
},
|
||||
"BookshopS3Settings": {
|
||||
"ServiceUrl": "http://192.168.1.177:30900",
|
||||
"Region": "garage",
|
||||
"BucketName": "bookshop",
|
||||
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
|
||||
},
|
||||
"Monitoring": {
|
||||
"ApiKey": "",
|
||||
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
|
||||
"ServiceName": "MidrandBooks.DEV"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Error"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user