refinement phase 0
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
<!-- Enables `dotnet user-secrets` for the local-dev connection string (never a committed secret). -->
|
||||
<UserSecretsId>baya-web-api</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Asp.Versioning;
|
||||
using Baya.Infrastructure.CrossCutting.Seams;
|
||||
using Baya.WebFramework.Attributes;
|
||||
using Baya.WebFramework.BaseController;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using AppModels = Baya.Application.Models.Common;
|
||||
|
||||
namespace Baya.Web.Api.Controllers.V1;
|
||||
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Display(Description = "Development-only helpers (return 404 outside the Development environment)")]
|
||||
public sealed class DevController(IHostEnvironment environment) : BaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// Development-only: returns the most recent OTP for <paramref name="phone"/> so a browser or an
|
||||
/// automated end-to-end flow can complete phone-OTP login without an SMS gateway (the code is otherwise
|
||||
/// only written to the server log by <c>LoggingSmsSender</c>). Returns 404 in every non-Development
|
||||
/// environment — the capture is not even wired there — so it can never leak a code in staging/production.
|
||||
/// It does not touch the OTP rate-limit or the per-phone resend window. Superseded by the real SMS gateway
|
||||
/// in refinement Phase 8.
|
||||
/// </summary>
|
||||
[HttpGet("[action]/{phone}")]
|
||||
[ProducesOkApiResponseType<DevLastOtpResult>]
|
||||
public IActionResult LastOtp(string phone)
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
return NotFound();
|
||||
|
||||
var code = HttpContext.RequestServices.GetService<DevOtpStore>()?.GetLatest(phone);
|
||||
|
||||
return code is null
|
||||
? OperationResult(AppModels.OperationResult<DevLastOtpResult>.NotFoundResult("No OTP has been issued for this phone yet."))
|
||||
: OperationResult(AppModels.OperationResult<DevLastOtpResult>.SuccessResult(new DevLastOtpResult(phone, code)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The most recent OTP captured for a phone (Development only).</summary>
|
||||
public record DevLastOtpResult(string Phone, string Code);
|
||||
@@ -72,8 +72,14 @@ builder.Services.AddApplicationServices()
|
||||
.AddPersistenceServices(configuration)
|
||||
.AddCrossCuttingSeams(configuration)
|
||||
.AddWebFrameworkServices()
|
||||
.AddCorsPolicies(configuration)
|
||||
.AddRateLimitingPolicies();
|
||||
|
||||
// Development-only: capture each OTP in-memory so GET /api/v1/dev/last_otp/{phone} can complete a login
|
||||
// without an SMS gateway. Nothing here is wired in any other environment.
|
||||
if (builder.Environment.IsDevelopment())
|
||||
builder.Services.AddDevelopmentOtpCapture();
|
||||
|
||||
builder.Services.RegisterValidatorsAsServices();
|
||||
builder.Services.AddExceptionHandler<ExceptionHandler>();
|
||||
|
||||
@@ -114,6 +120,10 @@ app.UseSwaggerAndUi();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
// After UseRouting and before the rate limiter / authentication so a pre-flight OPTIONS is answered
|
||||
// (and not rejected as 429/401) before the browser sends the real cross-origin request.
|
||||
app.UseCors(CorsServiceExtension.PolicyName);
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=87.107.152.16,1433;Database=Baya;User Id=sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;",
|
||||
"logDb":"Server=87.107.152.16,1433;Database=Baya_Logs;User Id=sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;"
|
||||
"SqlServer": "Server=localhost,1433;Database=Baya;User Id=sa;Password=SET_VIA_USER_SECRETS_OR_ENV;TrustServerCertificate=True;Encrypt=False;",
|
||||
"logDb": "Server=localhost,1433;Database=Baya_Logs;User Id=sa;Password=SET_VIA_USER_SECRETS_OR_ENV;TrustServerCertificate=True;Encrypt=False;"
|
||||
},
|
||||
"IdentitySettings": {
|
||||
"SecretKey": "ShouldBe-LongerThan-16Char-SecretKey",
|
||||
@@ -25,6 +25,9 @@
|
||||
"ResolvedConfidence": 0.9
|
||||
}
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [ "http://localhost:3000" ]
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=87.107.152.16,1433;Database=Baya;User Id=sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;",
|
||||
"logDb":"Server=87.107.152.16,1433;Database=Baya_Logs;User Id=sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;"
|
||||
"SqlServer": "Server=localhost,1433;Database=Baya;User Id=sa;Password=SET_VIA_USER_SECRETS_OR_ENV;TrustServerCertificate=True;Encrypt=False;",
|
||||
"logDb": "Server=localhost,1433;Database=Baya_Logs;User Id=sa;Password=SET_VIA_USER_SECRETS_OR_ENV;TrustServerCertificate=True;Encrypt=False;"
|
||||
},
|
||||
"IdentitySettings": {
|
||||
"SecretKey": "ShouldBe-LongerThan-16Char-SecretKey",
|
||||
@@ -25,6 +25,9 @@
|
||||
"ResolvedConfidence": 0.9
|
||||
}
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": []
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Baya.WebFramework.ServiceConfiguration;
|
||||
|
||||
public static class CorsServiceExtension
|
||||
{
|
||||
/// <summary>The single named CORS policy the browser SPA is allowed through. Registered in DI by
|
||||
/// <see cref="AddCorsPolicies"/> and applied in the pipeline by <c>app.UseCors(PolicyName)</c>.</summary>
|
||||
public const string PolicyName = "BalinyaarWebClient";
|
||||
|
||||
/// <summary>Configuration key holding the allowed browser origins (a string array).</summary>
|
||||
public const string AllowedOriginsKey = "Cors:AllowedOrigins";
|
||||
|
||||
/// <summary>Fallback origin when <see cref="AllowedOriginsKey"/> is unset — the Next.js client's default
|
||||
/// dev URL. A deployed environment lists its real web origin(s) in configuration.</summary>
|
||||
private const string DefaultDevelopmentOrigin = "http://localhost:3000";
|
||||
|
||||
// The client (client/src/lib/api/client.ts + the payment hooks) sets exactly these request headers on
|
||||
// its cross-origin calls; the pre-flight response must echo them back or the browser blocks the real
|
||||
// request. Kept explicit rather than AllowAnyHeader so the surface is auditable.
|
||||
private static readonly string[] AllowedHeaders =
|
||||
[
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"Accept-Language",
|
||||
"Idempotency-Key"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Registers the browser CORS policy from <see cref="AllowedOriginsKey"/> (a string array), falling back
|
||||
/// to the Next.js dev origin when unset so Development is permissive to localhost only. Credentials are
|
||||
/// NOT allowed: the client authenticates with a bearer <c>Authorization</c> header, not a cookie, so
|
||||
/// <c>AllowCredentials()</c> is unnecessary — and combining it with a wildcard origin is forbidden by the
|
||||
/// CORS spec anyway. Pair with <c>app.UseCors(<see cref="PolicyName"/>)</c> placed after
|
||||
/// <c>UseRouting()</c> and before the rate limiter / authentication, so a pre-flight OPTIONS is answered
|
||||
/// before those run.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCorsPolicies(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var origins = configuration.GetSection(AllowedOriginsKey).Get<string[]>();
|
||||
if (origins is null || origins.Length == 0)
|
||||
origins = [DefaultDevelopmentOrigin];
|
||||
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(PolicyName, policy =>
|
||||
policy.WithOrigins(origins)
|
||||
.WithHeaders(AllowedHeaders)
|
||||
.AllowAnyMethod());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user