Skip to main content

seamless_auth/
delivery.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use serde_json::Value;
5
6/// The error a delivery callback reports.
7pub type DeliveryError = Box<dyn std::error::Error + Send + Sync>;
8
9pub(crate) type DeliveryFuture = Pin<Box<dyn Future<Output = Result<(), DeliveryError>> + Send>>;
10
11/// A message the auth API hands back for the application to send.
12#[derive(Clone, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct Delivery {
15    /// `otp_email`, `otp_sms`, `magic_link_email` or `enrollment_invite_email`.
16    pub kind: String,
17    /// An email address or phone number.
18    pub to: String,
19    /// The one-time code, or the magic link's token.
20    pub token: Option<String>,
21    pub magic_link_url: Option<String>,
22    pub sign_in_url: Option<String>,
23}
24
25// The code and link are one-time credentials, so they stay out of logs that
26// print a Delivery.
27impl std::fmt::Debug for Delivery {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        let redacted = |v: &Option<String>| v.as_ref().map(|_| "[redacted]");
30        f.debug_struct("Delivery")
31            .field("kind", &self.kind)
32            .field("to", &self.to)
33            .field("token", &redacted(&self.token))
34            .field("magic_link_url", &redacted(&self.magic_link_url))
35            .field("sign_in_url", &self.sign_in_url)
36            .finish()
37    }
38}
39
40pub(crate) fn parse_delivery(raw: &Value) -> Result<Delivery, String> {
41    let obj = raw.as_object().ok_or("delivery is not an object")?;
42    let text = |name: &str| {
43        obj.get(name)
44            .and_then(Value::as_str)
45            .filter(|s| !s.is_empty())
46            .map(str::to_string)
47    };
48
49    let delivery = Delivery {
50        kind: text("kind").unwrap_or_default(),
51        to: text("to").unwrap_or_default(),
52        // An SMS code can arrive as a number.
53        token: match obj.get("token") {
54            Some(Value::String(s)) => Some(s.clone()),
55            Some(Value::Number(n)) => Some(n.to_string()),
56            _ => None,
57        },
58        magic_link_url: text("magicLinkUrl"),
59        sign_in_url: text("signInUrl"),
60    };
61    if delivery.kind.is_empty() || delivery.to.is_empty() {
62        return Err("delivery has no kind or recipient".into());
63    }
64    Ok(delivery)
65}