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
+64
View File
@@ -0,0 +1,64 @@
using JobsMedical.Web.Models;
namespace JobsMedical.Web.Services;
public interface ISmsSender
{
/// <summary>Send the OTP code. Returns false if not configured or the gateway call fails.</summary>
Task<bool> SendOtpAsync(string phone, string code, AppSetting settings, CancellationToken ct = default);
}
/// <summary>
/// Kavenegar SMS gateway (Iran). Prefers the verify/lookup API (a pre-approved OTP template, no
/// dedicated line needed); falls back to plain sms/send if only a sender line is configured.
/// Credentials live in AppSetting (admin panel), so no redeploy to set them.
/// </summary>
public class KavenegarSmsSender : ISmsSender
{
private readonly IHttpClientFactory _http;
private readonly ILogger<KavenegarSmsSender> _log;
public KavenegarSmsSender(IHttpClientFactory http, ILogger<KavenegarSmsSender> log)
{
_http = http;
_log = log;
}
public async Task<bool> SendOtpAsync(string phone, string code, AppSetting s, CancellationToken ct = default)
{
if (!s.SmsEnabled || string.IsNullOrWhiteSpace(s.SmsApiKey)) return false;
try
{
var client = _http.CreateClient("sms");
client.Timeout = TimeSpan.FromSeconds(15);
string url;
if (!string.IsNullOrWhiteSpace(s.SmsTemplate))
{
// verify/lookup: template contains %token → the code
url = $"https://api.kavenegar.com/v1/{s.SmsApiKey}/verify/lookup.json" +
$"?receptor={Uri.EscapeDataString(phone)}&token={Uri.EscapeDataString(code)}" +
$"&template={Uri.EscapeDataString(s.SmsTemplate)}";
}
else
{
var msg = $"کد ورود همکادر: {code}";
url = $"https://api.kavenegar.com/v1/{s.SmsApiKey}/sms/send.json" +
$"?receptor={Uri.EscapeDataString(phone)}&message={Uri.EscapeDataString(msg)}" +
(string.IsNullOrWhiteSpace(s.SmsSender) ? "" : $"&sender={Uri.EscapeDataString(s.SmsSender)}");
}
using var resp = await client.GetAsync(url, ct);
if (!resp.IsSuccessStatusCode)
{
_log.LogWarning("Kavenegar OTP HTTP {Status} for {Phone}", (int)resp.StatusCode, phone);
return false;
}
return true;
}
catch (Exception ex)
{
_log.LogWarning(ex, "Kavenegar OTP send failed for {Phone}", phone);
return false;
}
}
}