1use crate::errors::{AuthError, Result};
4use crate::storage::AuthStorage;
5use ring::rand::SecureRandom;
6use subtle::ConstantTimeEq;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use std::time::Duration;
10use tracing::{debug, error, info, warn};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SmsKitConfig {
16 pub provider: SmsKitProvider,
18 pub config: SmsKitProviderConfig,
20 pub fallback_provider: Option<SmsKitProvider>,
22 pub fallback_config: Option<SmsKitProviderConfig>,
24 pub webhook_config: Option<WebhookConfig>,
26 pub rate_limiting: RateLimitConfig,
28}
29
30impl Default for SmsKitConfig {
31 fn default() -> Self {
32 Self {
33 provider: SmsKitProvider::Development,
34 config: SmsKitProviderConfig::Development,
35 fallback_provider: None,
36 fallback_config: None,
37 webhook_config: None,
38 rate_limiting: RateLimitConfig::default(),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
49pub enum SmsKitProvider {
50 Twilio,
51 Plivo,
53 AwsSns,
55 Development,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub enum SmsKitProviderConfig {
61 Twilio {
62 account_sid: String,
63 auth_token: String,
64 from_number: String,
65 webhook_url: Option<String>,
66 },
67 Plivo {
68 auth_id: String,
69 auth_token: String,
70 from_number: String,
71 webhook_url: Option<String>,
72 },
73 AwsSns {
74 region: String,
75 access_key_id: String,
76 secret_access_key: String,
77 },
78 Development,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct WebhookConfig {
84 pub endpoint_url: String,
85 pub webhook_secret: String,
86 pub track_delivery: bool,
87 pub track_clicks: bool,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct RateLimitConfig {
93 pub max_per_hour: u32,
94 pub max_per_day: u32,
95 pub cooldown_seconds: u64,
96}
97
98impl Default for RateLimitConfig {
99 fn default() -> Self {
100 Self {
101 max_per_hour: 10,
102 max_per_day: 20,
103 cooldown_seconds: 60,
104 }
105 }
106}
107
108pub struct SmsKitManager {
110 storage: Arc<dyn AuthStorage>,
111 config: SmsKitConfig,
112}
113
114impl SmsKitManager {
115 pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
117 Self {
118 storage,
119 config: SmsKitConfig::default(),
120 }
121 }
122
123 pub fn new_with_config(storage: Arc<dyn AuthStorage>, config: SmsKitConfig) -> Result<Self> {
125 let manager = Self { storage, config };
126 Ok(manager)
127 }
128
129 pub async fn register_phone_number(&self, user_id: &str, phone_number: &str) -> Result<()> {
131 debug!("Registering phone number for user '{}' via SMSKit", user_id);
132
133 if phone_number.is_empty() {
134 return Err(AuthError::validation("Phone number cannot be empty"));
135 }
136
137 if !phone_number.starts_with('+') || phone_number.len() < 10 {
138 return Err(AuthError::validation(
139 "Phone number must be in international format (+1234567890)",
140 ));
141 }
142
143 let digits = &phone_number[1..];
144 if !digits.chars().all(|c| c.is_ascii_digit()) {
145 return Err(AuthError::validation(
146 "Phone number must contain only digits after the + sign",
147 ));
148 }
149
150 if digits.len() > 15 || digits.len() < 7 {
151 return Err(AuthError::validation(
152 "Phone number must be between 7 and 15 digits (E.164 format)",
153 ));
154 }
155
156 let key = format!("user:{}:phone", user_id);
157 self.storage
158 .store_kv(&key, phone_number.as_bytes(), None)
159 .await?;
160
161 info!(
162 "Phone number registered for user '{}': {} (SMSKit enabled)",
163 user_id, phone_number
164 );
165
166 Ok(())
167 }
168
169 pub async fn initiate_challenge(&self, user_id: &str) -> Result<String> {
171 debug!("Initiating SMS challenge for user '{}' via SMSKit", user_id);
172
173 if user_id.is_empty() {
174 return Err(AuthError::validation("User ID cannot be empty"));
175 }
176
177 self.check_rate_limits(user_id).await?;
178
179 let challenge_id = crate::utils::string::generate_id(Some("smskit"));
180
181 info!("SMS challenge initiated for user '{}' via SMSKit", user_id);
182 Ok(challenge_id)
183 }
184
185 async fn check_rate_limits(&self, user_id: &str) -> Result<()> {
186 let now = chrono::Utc::now().timestamp();
187 let hour_ago = now - 3600;
188 let day_ago = now - 86400;
189
190 let hourly_key = format!("smskit:{}:hourly", user_id);
191 let hourly_count = self.get_sms_count(&hourly_key, hour_ago).await?;
192 if hourly_count >= self.config.rate_limiting.max_per_hour {
193 return Err(AuthError::rate_limited("SMS hourly limit exceeded"));
194 }
195
196 let daily_key = format!("smskit:{}:daily", user_id);
197 let daily_count = self.get_sms_count(&daily_key, day_ago).await?;
198 if daily_count >= self.config.rate_limiting.max_per_day {
199 return Err(AuthError::rate_limited("SMS daily limit exceeded"));
200 }
201
202 let last_sent_key = format!("smskit:{}:last_sent", user_id);
203 if let Some(last_sent_data) = self.storage.get_kv(&last_sent_key).await?
204 && let Ok(last_sent_str) = std::str::from_utf8(&last_sent_data)
205 && let Ok(last_sent) = last_sent_str.parse::<i64>()
206 {
207 let elapsed = now - last_sent;
208 if elapsed < self.config.rate_limiting.cooldown_seconds as i64 {
209 let remaining = self.config.rate_limiting.cooldown_seconds as i64 - elapsed;
210 return Err(AuthError::rate_limited(format!(
211 "SMS cooldown active. Please wait {} seconds",
212 remaining
213 )));
214 }
215 }
216
217 Ok(())
218 }
219
220 async fn get_sms_count(&self, key: &str, _since: i64) -> Result<u32> {
221 if let Some(count_data) = self.storage.get_kv(key).await?
222 && let Ok(count_str) = std::str::from_utf8(&count_data)
223 && let Ok(count) = count_str.parse::<u32>()
224 {
225 return Ok(count);
226 }
227 Ok(0)
228 }
229
230 pub async fn generate_code(&self, challenge_id: &str) -> Result<String> {
232 debug!(
233 "Generating SMS code for challenge '{}' via SMSKit",
234 challenge_id
235 );
236
237 let rng = ring::rand::SystemRandom::new();
238 let mut buf = [0u8; 4];
239 rng.fill(&mut buf).expect("system RNG failure");
240 let val = u32::from_le_bytes(buf) % 1_000_000;
241 let code = format!("{:06}", val);
242
243 let sms_key = format!("smskit_challenge:{}:code", challenge_id);
244 self.storage
245 .store_kv(&sms_key, code.as_bytes(), Some(Duration::from_secs(300)))
246 .await?;
247
248 Ok(code)
249 }
250
251 pub async fn verify_code(&self, challenge_id: &str, code: &str) -> Result<bool> {
253 debug!(
254 "Verifying SMS code for challenge '{}' via SMSKit",
255 challenge_id
256 );
257
258 if challenge_id.is_empty() {
259 return Err(AuthError::validation("Challenge ID cannot be empty"));
260 }
261
262 if code.is_empty() {
263 return Err(AuthError::validation("SMS code cannot be empty"));
264 }
265
266 if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
267 return Ok(false);
268 }
269
270 let sms_key = format!("smskit_challenge:{}:code", challenge_id);
271 if let Some(stored_code_data) = self.storage.get_kv(&sms_key).await? {
272 let stored_code = std::str::from_utf8(&stored_code_data).unwrap_or("");
273 let is_valid: bool = stored_code.as_bytes().ct_eq(code.as_bytes()).into();
274
275 if is_valid {
276 let _ = self.storage.delete_kv(&sms_key).await;
277 }
278
279 Ok(is_valid)
280 } else {
281 Err(AuthError::validation("Invalid or expired challenge ID"))
282 }
283 }
284
285 pub async fn send_code(&self, user_id: &str, code: &str) -> Result<()> {
287 debug!("Sending SMS code to user '{}' via SMSKit", user_id);
288
289 let phone_key = format!("user:{}:phone", user_id);
290 let phone_number = if let Some(phone_data) = self.storage.get_kv(&phone_key).await? {
291 String::from_utf8(phone_data)
292 .map_err(|e| AuthError::internal(format!("Failed to parse phone number: {}", e)))?
293 } else {
294 return Err(AuthError::validation("No phone number registered for user"));
295 };
296
297 self.check_rate_limits(user_id).await?;
298
299 let message = format!(
300 "Your verification code is: {}. This code expires in 5 minutes. Do not share this code with anyone.",
301 code
302 );
303
304 match self.send_sms_with_fallback(&phone_number, &message).await {
305 Ok(message_id) => {
306 info!(
307 "SMS code sent successfully to user '{}' (Message ID: {})",
308 user_id, message_id
309 );
310 self.update_rate_limits(user_id).await?;
311 Ok(())
312 }
313 Err(e) => {
314 error!("Failed to send SMS to user '{}': {}", user_id, e);
315 Err(e)
316 }
317 }
318 }
319
320 async fn send_sms_with_fallback(&self, phone_number: &str, message: &str) -> Result<String> {
321 let result = match &self.config.provider {
322 SmsKitProvider::Twilio => self.send_via_twilio(phone_number, message).await,
323 SmsKitProvider::Plivo => self.send_via_plivo(phone_number, message).await,
324 SmsKitProvider::AwsSns => self.send_via_aws_sns(phone_number, message).await,
325 SmsKitProvider::Development => {
326 info!("📱 [SMSKit Development] SMS sent to: {}", phone_number);
327 info!(" Message: {}", message);
328 Ok(format!("dev_msg_{}", chrono::Utc::now().timestamp()))
329 }
330 };
331
332 match result {
334 Ok(msg_id) => Ok(msg_id),
335 Err(primary_err) => {
336 if let Some(fallback_provider) = &self.config.fallback_provider {
337 warn!(
338 "Primary SMS provider failed ({}), trying fallback: {:?}",
339 primary_err, fallback_provider
340 );
341 match fallback_provider {
342 SmsKitProvider::Development => {
343 info!(
344 "📱 [SMSKit Development Fallback] SMS sent to: {}",
345 phone_number
346 );
347 info!(" Message: {}", message);
348 Ok(format!(
349 "dev_fallback_msg_{}",
350 chrono::Utc::now().timestamp()
351 ))
352 }
353 _ => Err(primary_err),
354 }
355 } else {
356 Err(primary_err)
357 }
358 }
359 }
360 }
361
362 #[cfg(feature = "smskit")]
364 async fn send_via_twilio(&self, phone_number: &str, message: &str) -> Result<String> {
365 use sms_core::SmsClient;
366
367 let (client, from_number) = if let SmsKitProviderConfig::Twilio {
368 account_sid,
369 auth_token,
370 from_number,
371 ..
372 } = &self.config.config
373 {
374 if account_sid.is_empty() || auth_token.is_empty() || from_number.is_empty() {
375 return Err(AuthError::internal("Twilio credentials are incomplete"));
376 }
377 (
378 sms_twilio::TwilioClient::new(account_sid, auth_token),
379 from_number.clone(),
380 )
381 } else {
382 let from_number = std::env::var("TWILIO_FROM_NUMBER").map_err(|_| {
383 AuthError::internal("Twilio from number not configured: set TWILIO_FROM_NUMBER")
384 })?;
385 let c = sms_twilio::TwilioClient::from_env()
386 .map_err(|e| AuthError::internal(format!("Twilio env config failed: {}", e)))?;
387 (c, from_number)
388 };
389
390 let request = sms_core::SendRequest {
391 to: phone_number,
392 from: &from_number,
393 text: message,
394 };
395
396 let response = client
397 .send(request)
398 .await
399 .map_err(|e| AuthError::internal(format!("Twilio SMS send failed: {}", e)))?;
400
401 debug!("Twilio SMS sent successfully, ID: {}", response.id);
402 Ok(response.id)
403 }
404
405 #[cfg(not(feature = "smskit"))]
406 async fn send_via_twilio(&self, _phone_number: &str, _message: &str) -> Result<String> {
407 Err(AuthError::internal(
408 "Twilio SMS requires the 'smskit' feature flag to be enabled",
409 ))
410 }
411
412 #[cfg(feature = "smskit")]
414 async fn send_via_plivo(&self, phone_number: &str, message: &str) -> Result<String> {
415 use sms_core::SmsClient;
416
417 let (client, from_number) = if let SmsKitProviderConfig::Plivo {
418 auth_id,
419 auth_token,
420 from_number,
421 ..
422 } = &self.config.config
423 {
424 if auth_id.is_empty() || auth_token.is_empty() || from_number.is_empty() {
425 return Err(AuthError::internal("Plivo credentials are incomplete"));
426 }
427 (
428 sms_plivo::PlivoClient::new(auth_id, auth_token),
429 from_number.clone(),
430 )
431 } else {
432 let from_number = std::env::var("PLIVO_FROM_NUMBER").map_err(|_| {
433 AuthError::internal("Plivo from number not configured: set PLIVO_FROM_NUMBER")
434 })?;
435 let c = sms_plivo::PlivoClient::from_env()
436 .map_err(|e| AuthError::internal(format!("Plivo env config failed: {}", e)))?;
437 (c, from_number)
438 };
439
440 let request = sms_core::SendRequest {
441 to: phone_number,
442 from: &from_number,
443 text: message,
444 };
445
446 let response = client
447 .send(request)
448 .await
449 .map_err(|e| AuthError::internal(format!("Plivo SMS send failed: {}", e)))?;
450
451 debug!("Plivo SMS sent successfully, ID: {}", response.id);
452 Ok(response.id)
453 }
454
455 #[cfg(not(feature = "smskit"))]
456 async fn send_via_plivo(&self, _phone_number: &str, _message: &str) -> Result<String> {
457 Err(AuthError::internal(
458 "Plivo SMS requires the 'smskit' feature flag to be enabled",
459 ))
460 }
461
462 #[cfg(feature = "smskit")]
464 async fn send_via_aws_sns(&self, phone_number: &str, message: &str) -> Result<String> {
465 use sms_core::SmsClient;
466
467 let client = if let SmsKitProviderConfig::AwsSns {
468 region,
469 access_key_id,
470 secret_access_key,
471 } = &self.config.config
472 {
473 if access_key_id.is_empty() || secret_access_key.is_empty() {
474 return Err(AuthError::internal("AWS credentials are incomplete"));
475 }
476 sms_aws_sns::AwsSnsClient::new(region, access_key_id, secret_access_key)
477 } else {
478 sms_aws_sns::AwsSnsClient::from_env()
479 .map_err(|e| AuthError::internal(format!("AWS SNS env config failed: {}", e)))?
480 };
481
482 let request = sms_core::SendRequest {
483 to: phone_number,
484 from: "",
485 text: message,
486 };
487
488 let response = client
489 .send(request)
490 .await
491 .map_err(|e| AuthError::internal(format!("AWS SNS SMS send failed: {}", e)))?;
492
493 debug!("AWS SNS SMS sent successfully, ID: {}", response.id);
494 Ok(response.id)
495 }
496
497 #[cfg(not(feature = "smskit"))]
498 async fn send_via_aws_sns(&self, _phone_number: &str, _message: &str) -> Result<String> {
499 Err(AuthError::internal(
500 "AWS SNS SMS requires the 'smskit' feature flag to be enabled",
501 ))
502 }
503
504 async fn update_rate_limits(&self, user_id: &str) -> Result<()> {
505 let now = chrono::Utc::now().timestamp();
506
507 let hourly_key = format!("smskit:{}:hourly", user_id);
508 let hourly_count = self.get_sms_count(&hourly_key, now - 3600).await? + 1;
509 self.storage
510 .store_kv(
511 &hourly_key,
512 hourly_count.to_string().as_bytes(),
513 Some(Duration::from_secs(3600)),
514 )
515 .await?;
516
517 let daily_key = format!("smskit:{}:daily", user_id);
518 let daily_count = self.get_sms_count(&daily_key, now - 86400).await? + 1;
519 self.storage
520 .store_kv(
521 &daily_key,
522 daily_count.to_string().as_bytes(),
523 Some(Duration::from_secs(86400)),
524 )
525 .await?;
526
527 let last_sent_key = format!("smskit:{}:last_sent", user_id);
528 self.storage
529 .store_kv(
530 &last_sent_key,
531 now.to_string().as_bytes(),
532 Some(Duration::from_secs(
533 self.config.rate_limiting.cooldown_seconds,
534 )),
535 )
536 .await?;
537
538 Ok(())
539 }
540
541 pub async fn get_user_phone(&self, user_id: &str) -> Result<Option<String>> {
543 let phone_key = format!("user:{}:phone", user_id);
544
545 if let Some(phone_data) = self.storage.get_kv(&phone_key).await? {
546 Ok(Some(String::from_utf8(phone_data).map_err(|e| {
547 AuthError::internal(format!("Failed to parse phone number: {}", e))
548 })?))
549 } else {
550 Ok(None)
551 }
552 }
553
554 pub async fn has_phone_number(&self, user_id: &str) -> Result<bool> {
556 let key = format!("user:{}:phone", user_id);
557 match self.storage.get_kv(&key).await {
558 Ok(Some(_)) => Ok(true),
559 Ok(None) => Ok(false),
560 Err(_) => Ok(false), }
562 }
563
564 pub async fn send_verification_code(&self, user_id: &str) -> Result<String> {
566 let code = format!("{:06}", rand::random::<u32>() % 1_000_000);
568
569 self.send_code(user_id, &code).await?;
571
572 let code_key = format!("sms_verification:{}:{}", user_id, Uuid::new_v4());
574 self.storage
575 .store_kv(
576 &code_key,
577 code.as_bytes(),
578 Some(std::time::Duration::from_secs(300)),
579 )
580 .await?;
581
582 Ok(code)
583 }
584}