Skip to main content

txline/
auth.rs

1//! Guest JWTs, activated API tokens, and activation preimages.
2
3use std::fmt;
4
5use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
6use serde::{Deserialize, Serialize};
7
8use crate::{Result, TxlineError};
9
10pub const API_TOKEN_HEADER: &str = "X-Api-Token";
11
12#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct GuestJwt(String);
14
15impl GuestJwt {
16    /// Create a guest JWT, trimming leading and trailing whitespace.
17    ///
18    /// Empty and whitespace-only values are rejected.
19    pub fn new(token: impl Into<String>) -> Result<Self> {
20        let token = token.into().trim().to_owned();
21        if token.is_empty() {
22            return Err(TxlineError::invalid_input("guest JWT must not be empty"));
23        }
24        Ok(Self(token))
25    }
26
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31
32impl fmt::Debug for GuestJwt {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.write_str("GuestJwt(<redacted>)")
35    }
36}
37
38#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct ApiToken(String);
40
41impl ApiToken {
42    /// Create an activated API token, trimming leading and trailing whitespace.
43    ///
44    /// Empty and whitespace-only values are rejected.
45    pub fn new(token: impl Into<String>) -> Result<Self> {
46        let token = token.into().trim().to_owned();
47        if token.is_empty() {
48            return Err(TxlineError::invalid_input("API token must not be empty"));
49        }
50        Ok(Self(token))
51    }
52
53    pub fn as_str(&self) -> &str {
54        &self.0
55    }
56}
57
58impl fmt::Debug for ApiToken {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str("ApiToken(<redacted>)")
61    }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct GuestSession {
66    pub token: GuestJwt,
67}
68
69#[derive(Clone, PartialEq, Eq)]
70pub struct AuthHeaders {
71    authorization: GuestJwt,
72    api_token: Option<ApiToken>,
73}
74
75impl AuthHeaders {
76    pub fn new(authorization: GuestJwt, api_token: Option<ApiToken>) -> Self {
77        Self {
78            authorization,
79            api_token,
80        }
81    }
82
83    pub fn to_header_map(&self) -> Result<HeaderMap> {
84        let mut headers = HeaderMap::new();
85        let auth_value = format!("Bearer {}", self.authorization.as_str());
86        headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
87        if let Some(api_token) = &self.api_token {
88            headers.insert(API_TOKEN_HEADER, HeaderValue::from_str(api_token.as_str())?);
89        }
90        Ok(headers)
91    }
92
93    pub fn has_api_token(&self) -> bool {
94        self.api_token.is_some()
95    }
96}
97
98impl fmt::Debug for AuthHeaders {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.debug_struct("AuthHeaders")
101            .field("authorization", &"<redacted>")
102            .field("api_token", &self.api_token.as_ref().map(|_| "<redacted>"))
103            .finish()
104    }
105}
106
107#[derive(Debug, Deserialize)]
108pub(crate) struct TokenResponse {
109    pub token: String,
110}
111
112#[derive(Debug, Serialize)]
113pub(crate) struct ActivationPayload<'a> {
114    #[serde(rename = "txSig")]
115    pub tx_sig: &'a str,
116    #[serde(rename = "walletSignature")]
117    pub wallet_signature: &'a str,
118    pub leagues: &'a [i32],
119}
120
121/// Build the exact message that must be signed for `/api/token/activate`.
122///
123/// Empty league lists intentionally produce `txSig::jwt`.
124pub fn activation_preimage(
125    tx_sig: impl AsRef<str>,
126    selected_leagues: &[i32],
127    jwt: &GuestJwt,
128) -> String {
129    let leagues = selected_leagues
130        .iter()
131        .map(i32::to_string)
132        .collect::<Vec<_>>()
133        .join(",");
134    format!("{}:{}:{}", tx_sig.as_ref(), leagues, jwt.as_str())
135}