6f02b1a0e9
Add docker-compose.local.yml + Dockerfile.local (public MS images + Liara NuGet) to run the whole app with a throwaway Postgres in one command for local testing, plus LOCAL.md. OtpService now never calls Kavenegar in the Development environment and always returns the code so the login page shows it on screen — guarantees local logins work with no SMS. Production behavior unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
2.5 KiB
C#
68 lines
2.5 KiB
C#
using JobsMedical.Web.Services.Scraping;
|
||
using Microsoft.Extensions.Caching.Memory;
|
||
using Microsoft.Extensions.Hosting;
|
||
|
||
namespace JobsMedical.Web.Services;
|
||
|
||
/// <summary>
|
||
/// One-time-code issuing/verification. Codes live in memory for 5 minutes. When SMS is configured
|
||
/// (admin settings) the code is sent via the gateway and NOT returned; otherwise it's returned so
|
||
/// the dev login page can display it.
|
||
/// </summary>
|
||
public class OtpService
|
||
{
|
||
private readonly IMemoryCache _cache;
|
||
private readonly ISmsSender _sms;
|
||
private readonly SettingsService _settings;
|
||
private readonly IHostEnvironment _env;
|
||
|
||
public OtpService(IMemoryCache cache, ISmsSender sms, SettingsService settings, IHostEnvironment env)
|
||
{
|
||
_cache = cache;
|
||
_sms = sms;
|
||
_settings = settings;
|
||
_env = env;
|
||
}
|
||
|
||
private static string Key(string phone) => $"otp:{Normalize(phone)}";
|
||
|
||
/// <summary>
|
||
/// Generate + store a 5-digit code. If SMS is enabled, send it and return null (don't reveal);
|
||
/// otherwise return the code so the dev login screen can show it.
|
||
/// </summary>
|
||
public async Task<string?> IssueAsync(string phone)
|
||
{
|
||
var code = Random.Shared.Next(10000, 100000).ToString();
|
||
_cache.Set(Key(phone), code, TimeSpan.FromMinutes(5));
|
||
|
||
var settings = await _settings.GetAsync();
|
||
// In Development (local Docker / dotnet run) never call the SMS gateway — always show the
|
||
// code on the login screen. In Production, send via SMS only when it's enabled.
|
||
if (settings.SmsEnabled && !_env.IsDevelopment())
|
||
{
|
||
await _sms.SendOtpAsync(phone, code, settings);
|
||
return null; // sent via SMS — don't reveal it
|
||
}
|
||
return code; // local/dev (or SMS off): surface it on screen
|
||
}
|
||
|
||
public bool Verify(string phone, string code)
|
||
{
|
||
if (_cache.TryGetValue(Key(phone), out string? stored) && stored == code?.Trim())
|
||
{
|
||
_cache.Remove(Key(phone));
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>Normalize Iranian mobile numbers (Persian digits → Latin, strip spaces).</summary>
|
||
public static string Normalize(string phone)
|
||
{
|
||
var chars = phone.Trim().ToCharArray();
|
||
for (var i = 0; i < chars.Length; i++)
|
||
if (chars[i] >= '۰' && chars[i] <= '۹') chars[i] = (char)('0' + (chars[i] - '۰'));
|
||
return new string(chars).Replace(" ", "").Replace("-", "");
|
||
}
|
||
}
|