Add SEO sitemap/robots + real SMS OTP (Kavenegar, admin-configured)
CI/CD / CI · dotnet build (push) Successful in 31s
CI/CD / Deploy · hamkadr (push) Successful in 56s

- /sitemap.xml (static pages + open shifts + fresh jobs, respecting expiry) + /robots.txt (blocks /Admin,/Employer); base URL from forwarded request → https://hamkadr.ir in prod
- ISmsSender + KavenegarSmsSender (verify/lookup template, sms/send fallback); SMS settings (enabled/apikey/template/sender) in /Admin/Settings; OtpService.IssueAsync sends SMS and stops revealing the code when enabled (dev still shows it); migration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 10:27:21 +03:30
parent 6d2ad6f87e
commit 17d38431bf
12 changed files with 1152 additions and 11 deletions
+26 -8
View File
@@ -1,26 +1,44 @@
using JobsMedical.Web.Services.Scraping;
using Microsoft.Extensions.Caching.Memory;
namespace JobsMedical.Web.Services;
/// <summary>
/// One-time-code issuing/verification. Codes live in memory for 5 minutes. In dev the code is
/// returned to the caller so it can be shown on screen; in production this is where an Iranian
/// SMS gateway (Kavenegar / SMS.ir) would send the code instead.
/// 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;
public OtpService(IMemoryCache cache) => _cache = cache;
private readonly ISmsSender _sms;
private readonly SettingsService _settings;
public OtpService(IMemoryCache cache, ISmsSender sms, SettingsService settings)
{
_cache = cache;
_sms = sms;
_settings = settings;
}
private static string Key(string phone) => $"otp:{Normalize(phone)}";
/// <summary>Generate, store, and (in dev) return a 5-digit code for the phone.</summary>
public string Issue(string 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));
// TODO(prod): send `code` via Kavenegar/SMS.ir instead of returning it.
return code;
var settings = await _settings.GetAsync();
if (settings.SmsEnabled)
{
await _sms.SendOtpAsync(phone, code, settings);
return null; // never reveal the code in production
}
return code; // dev: surface it on screen
}
public bool Verify(string phone, string code)