refinement phase 0
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
using Baya.Application.Contracts.Common;
|
||||
|
||||
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||
|
||||
/// <summary>
|
||||
/// Development-only <see cref="ISmsSender"/> decorator: forwards to the real (mock) sender so the code is
|
||||
/// still logged, and additionally captures it in <see cref="DevOtpStore"/> so the Development-only
|
||||
/// <c>/api/v1/dev/last_otp/{phone}</c> endpoint can serve it to a browser / e2e test. Registered ONLY in the
|
||||
/// Development environment (see <c>DevelopmentSeamExtensions.AddDevelopmentOtpCapture</c>); it changes no
|
||||
/// auth behaviour — the OTP is generated, validated and rate-limited exactly as before.
|
||||
/// </summary>
|
||||
public sealed class DevCapturingSmsSender(ISmsSender inner, DevOtpStore store) : ISmsSender
|
||||
{
|
||||
public Task SendOtpAsync(string phone, string code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
store.Capture(phone, code);
|
||||
return inner.SendOtpAsync(phone, code, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendAsync(string phone, string message, CancellationToken cancellationToken = default)
|
||||
=> inner.SendAsync(phone, message, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||
|
||||
/// <summary>
|
||||
/// Development-only, in-memory capture of the most recent OTP per phone. It exists so a browser or an
|
||||
/// automated end-to-end test can complete phone-OTP login without a real SMS gateway — OTP "delivery" is
|
||||
/// <see cref="LoggingSmsSender"/>, which only writes the code to the server log. It is populated ONLY when
|
||||
/// <see cref="DevCapturingSmsSender"/> is registered, which happens only in the Development environment (see
|
||||
/// <c>DevelopmentSeamExtensions.AddDevelopmentOtpCapture</c>). It is never wired outside Development, and the
|
||||
/// endpoint that exposes it (<c>/api/v1/dev/last_otp/{phone}</c>) is additionally gated on
|
||||
/// <c>IHostEnvironment.IsDevelopment()</c>. Superseded by the real SMS gateway in refinement Phase 8.
|
||||
/// </summary>
|
||||
public sealed class DevOtpStore
|
||||
{
|
||||
// Bounded so a long-lived dev session can't grow the map without limit; only the newest code per phone
|
||||
// matters for completing a login.
|
||||
private const int MaxEntries = 500;
|
||||
|
||||
private readonly ConcurrentDictionary<string, string> _codesByPhone = new();
|
||||
|
||||
public void Capture(string phone, string code)
|
||||
{
|
||||
var key = NormalizeKey(phone);
|
||||
if (key is null)
|
||||
return;
|
||||
|
||||
if (_codesByPhone.Count >= MaxEntries && !_codesByPhone.ContainsKey(key))
|
||||
_codesByPhone.Clear();
|
||||
|
||||
_codesByPhone[key] = code;
|
||||
}
|
||||
|
||||
public string? GetLatest(string phone)
|
||||
{
|
||||
var key = NormalizeKey(phone);
|
||||
return key is not null && _codesByPhone.TryGetValue(key, out var code) ? code : null;
|
||||
}
|
||||
|
||||
// Digits-only, last 10 → matches IranianPhone's canonical 09xxxxxxxxx regardless of how the caller
|
||||
// spelled it (+98 / 0098 / 98 / 0 prefix). Kept self-contained so this dev helper needs no dependency on
|
||||
// the Application layer's internal phone normalizer.
|
||||
private static string? NormalizeKey(string? phone)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(phone))
|
||||
return null;
|
||||
|
||||
var digits = new string(phone.Where(char.IsAsciiDigit).ToArray());
|
||||
return digits.Length >= 10 ? digits[^10..] : null;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Baya.Infrastructure.CrossCutting.Seams;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Baya.Infrastructure.CrossCutting.ServiceConfiguration;
|
||||
|
||||
public static class DevelopmentSeamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Development-only wiring for the OTP bring-up bridge. Registers <see cref="DevOtpStore"/> and decorates
|
||||
/// the <see cref="ISmsSender"/> registered by <c>AddCrossCuttingSeams</c> (the log-only
|
||||
/// <see cref="LoggingSmsSender"/>) with <see cref="DevCapturingSmsSender"/>, so each OTP is also captured
|
||||
/// in memory for <c>/api/v1/dev/last_otp/{phone}</c> to serve. MUST be called only inside
|
||||
/// <c>builder.Environment.IsDevelopment()</c>: nothing here is wired in any other environment, which —
|
||||
/// together with the endpoint's own <c>IsDevelopment()</c> guard — makes the OTP echo impossible to enable
|
||||
/// outside Development. Superseded by the real SMS gateway in refinement Phase 8.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddDevelopmentOtpCapture(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<DevOtpStore>();
|
||||
|
||||
// Re-register ISmsSender as the capturing decorator over a fresh LoggingSmsSender (built through DI so
|
||||
// it still gets its ILogger). The last registration wins for a single resolve, so callers transparently
|
||||
// get the decorator; the code is still logged exactly as before, just also captured for the dev endpoint.
|
||||
services.AddSingleton<ISmsSender>(sp => new DevCapturingSmsSender(
|
||||
ActivatorUtilities.CreateInstance<LoggingSmsSender>(sp),
|
||||
sp.GetRequiredService<DevOtpStore>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user