use crate::error::MfaError;
use crate::mfa::{MfaPrompt, MfaResponse, MfaSource};
use async_trait::async_trait;
use reqwest::Url;
use serde::Deserialize;
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug, Clone)]
pub struct SmsRelayMfaSource {
api_url: Url,
phone_number: String,
poll_interval: Duration,
max_attempts: usize,
}
#[derive(Debug, Deserialize)]
struct SmsMessage {
id: String,
to: String,
body: String,
}
impl SmsRelayMfaSource {
#[must_use]
pub fn new(api_url: Url, phone_number: impl Into<String>) -> Self {
Self {
api_url,
phone_number: phone_number.into(),
poll_interval: Duration::from_secs(2),
max_attempts: 15,
}
}
#[must_use]
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
self.poll_interval = interval;
self
}
#[must_use]
pub fn with_max_attempts(mut self, attempts: usize) -> Self {
self.max_attempts = attempts;
self
}
}
#[async_trait]
impl MfaSource for SmsRelayMfaSource {
async fn fetch(&self, _prompt: &MfaPrompt) -> Result<MfaResponse, MfaError> {
let client = crate::mfa::polling_client()?;
for _ in 0..self.max_attempts {
let mut msgs_url = self.api_url.clone();
msgs_url.set_path("/messages");
let resp = match client.get(msgs_url).send().await {
Ok(r) => r,
Err(e) => return Err(MfaError::Email(format!("sms network error: {}", e))),
};
let messages: Vec<SmsMessage> = match resp.json().await {
Ok(m) => m,
Err(e) => return Err(MfaError::Email(format!("sms parse error: {}", e))),
};
for msg in messages {
if msg.to.contains(&self.phone_number) {
if let Some(code) = extract_otp_from_sms(&msg.body) {
let mut del_url = self.api_url.clone();
del_url.set_path(&format!("/messages/{}", msg.id));
let _ = client.delete(del_url).send().await;
return Ok(MfaResponse { code });
}
}
}
sleep(self.poll_interval).await;
}
Err(MfaError::Email("polling timed out waiting for sms".into()))
}
}
fn extract_otp_from_sms(body: &str) -> Option<String> {
let mut current_digits = String::new();
for c in body.chars() {
if c.is_ascii_digit() {
current_digits.push(c);
} else {
if current_digits.len() == 6 {
return Some(current_digits);
}
current_digits.clear();
}
}
if current_digits.len() == 6 {
return Some(current_digits);
}
None
}