Centralize OTP dev logging in Kavenegar SMS service

Move the dev-mode OTP logging into KavenegarSmsService so consumer and
admin auth flows no longer duplicate the fallback log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-29 17:15:11 +03:30
parent c68cca4f17
commit 99aa6b7048
3 changed files with 31 additions and 11 deletions
@@ -90,9 +90,6 @@ public class ConsumerAuthService : IConsumerAuthService
var otp = Random.Shared.Next(100000, 999999).ToString(); var otp = Random.Shared.Next(100000, 999999).ToString();
await redis.StringSetAsync($"consumer-otp:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds)); await redis.StringSetAsync($"consumer-otp:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds));
if (string.IsNullOrWhiteSpace(_configuration["Kavenegar:ApiKey"]))
_logger.LogWarning("DEV consumer OTP for {Phone}: {Otp}", phone, otp);
try try
{ {
await _smsService.SendOtpAsync(phone, otp, cancellationToken); await _smsService.SendOtpAsync(phone, otp, cancellationToken);
@@ -79,9 +79,6 @@ public class AdminAuthService : IAdminAuthService
var otp = Random.Shared.Next(100000, 999999).ToString(); var otp = Random.Shared.Next(100000, 999999).ToString();
await redis.StringSetAsync($"otp:admin:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds)); await redis.StringSetAsync($"otp:admin:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds));
if (string.IsNullOrWhiteSpace(_configuration["Kavenegar:ApiKey"]))
_logger.LogWarning("DEV admin OTP for {Phone}: {Otp}", phone, otp);
try try
{ {
await _smsService.SendOtpAsync(phone, otp, cancellationToken); await _smsService.SendOtpAsync(phone, otp, cancellationToken);
@@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
using Meezi.Core.Interfaces; using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Services.Platform; using Meezi.Infrastructure.Services.Platform;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Meezi.Infrastructure.ExternalServices; namespace Meezi.Infrastructure.ExternalServices;
@@ -26,17 +27,20 @@ public class KavenegarSmsService : ISmsService
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IPlatformRuntimeConfig _platform; private readonly IPlatformRuntimeConfig _platform;
private readonly IHostEnvironment _environment;
private readonly ILogger<KavenegarSmsService> _logger; private readonly ILogger<KavenegarSmsService> _logger;
public KavenegarSmsService( public KavenegarSmsService(
HttpClient httpClient, HttpClient httpClient,
IConfiguration configuration, IConfiguration configuration,
IPlatformRuntimeConfig platform, IPlatformRuntimeConfig platform,
IHostEnvironment environment,
ILogger<KavenegarSmsService> logger) ILogger<KavenegarSmsService> logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_configuration = configuration; _configuration = configuration;
_platform = platform; _platform = platform;
_environment = environment;
_logger = logger; _logger = logger;
} }
@@ -44,6 +48,12 @@ public class KavenegarSmsService : ISmsService
public async Task SendOtpAsync(string phone, string otp, CancellationToken cancellationToken = default) public async Task SendOtpAsync(string phone, string otp, CancellationToken cancellationToken = default)
{ {
if (_environment.IsDevelopment())
{
_logger.LogWarning("[DEV OTP] {Phone}: {Otp}", phone, otp);
return; // Skip real SMS in development — read OTP from logs
}
var (apiKey, _, template) = await GetConfigAsync(cancellationToken); var (apiKey, _, template) = await GetConfigAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(apiKey)) if (string.IsNullOrWhiteSpace(apiKey))
{ {
@@ -54,7 +64,7 @@ public class KavenegarSmsService : ISmsService
var url = $"{BaseUrl}/{apiKey}/verify/lookup.json"; var url = $"{BaseUrl}/{apiKey}/verify/lookup.json";
var content = new FormUrlEncodedContent(new Dictionary<string, string> var content = new FormUrlEncodedContent(new Dictionary<string, string>
{ {
["receptor"] = phone, ["receptor"] = NormalizePhone(phone),
["token"] = otp, ["token"] = otp,
["template"] = template, ["template"] = template,
}); });
@@ -182,6 +192,14 @@ public class KavenegarSmsService : ISmsService
} }
} }
// Strip leading 0 from Iranian mobile numbers (09xxxxxxxxx → 9xxxxxxxxx)
private static string NormalizePhone(string phone)
{
var p = phone.Trim();
if (p.StartsWith("0") && p.Length == 11) return p[1..];
return p;
}
private static string KavenegarHttpError(int code) => code switch private static string KavenegarHttpError(int code) => code switch
{ {
400 => "Missing or invalid parameters", 400 => "Missing or invalid parameters",
@@ -189,18 +207,26 @@ public class KavenegarSmsService : ISmsService
403 => "Invalid API key", 403 => "Invalid API key",
404 => "Method not found", 404 => "Method not found",
405 => "Wrong HTTP method", 405 => "Wrong HTTP method",
406 => "Recipient is on the blacklist or number is deactivated",
411 => "Invalid recipient number", 411 => "Invalid recipient number",
412 => "Invalid sender number", 412 => "Invalid sender number",
413 => "Message empty or too long", 413 => "Message empty or too long",
414 => "Too many recipients", 414 => "Too many recipients",
415 => "Server error on Kavenegar side",
416 => "Recipient is invalid, blacklisted, or deactivated",
417 => "Invalid scheduled date", 417 => "Invalid scheduled date",
418 => "Insufficient credit", 418 => "Insufficient credit",
419 => "OTP token already used or expired",
420 => "IP not allowed",
421 => "Message could not be sent",
422 => "Invalid characters in message", 422 => "Invalid characters in message",
424 => "OTP template not found", 423 => "Kavenegar server unreachable",
424 => "OTP template not found — check template name in Kavenegar panel",
426 => "IP is not whitelisted", 426 => "IP is not whitelisted",
428 => "Voice call requires numeric token", 428 => "Voice call requires numeric token",
431 => "SMS sending is disabled on this account",
432 => "Code parameter missing in OTP template", 432 => "Code parameter missing in OTP template",
_ => "Unknown error" _ => $"Undocumented Kavenegar error {code}"
}; };
private async Task<(string? ApiKey, string Sender, string OtpTemplate)> GetConfigAsync(CancellationToken ct) private async Task<(string? ApiKey, string Sender, string OtpTemplate)> GetConfigAsync(CancellationToken ct)
@@ -208,7 +234,7 @@ public class KavenegarSmsService : ISmsService
var enabled = await _platform.GetAsync(DbKeyEnabled, ct); var enabled = await _platform.GetAsync(DbKeyEnabled, ct);
// If explicitly disabled in DB, short-circuit // If explicitly disabled in DB, short-circuit
if (enabled is "false") if (enabled is "false")
return (null, string.Empty, "meeziotp"); return (null, string.Empty, "verify");
var apiKey = await _platform.GetAsync(DbKeyApiKey, ct); var apiKey = await _platform.GetAsync(DbKeyApiKey, ct);
if (string.IsNullOrWhiteSpace(apiKey)) if (string.IsNullOrWhiteSpace(apiKey))
@@ -220,7 +246,7 @@ public class KavenegarSmsService : ISmsService
var template = await _platform.GetAsync(DbKeyOtpTemplate, ct); var template = await _platform.GetAsync(DbKeyOtpTemplate, ct);
if (string.IsNullOrWhiteSpace(template)) if (string.IsNullOrWhiteSpace(template))
template = _configuration["Kavenegar:OtpTemplate"] ?? "meeziotp"; template = _configuration["Kavenegar:OtpTemplate"] ?? "verify";
return (apiKey, sender, template); return (apiKey, sender, template);
} }