rspotify_s_model/
auth.rs

1//! All objects related to the auth flows defined by Spotify API
2
3use crate::{
4    custom_serde::{duration_second, space_separated_scopes},
5    ModelResult,
6};
7
8use std::{
9    collections::{HashMap, HashSet},
10    fs,
11    io::{Read, Write},
12    path::Path,
13};
14
15use chrono::{DateTime, Duration, TimeDelta, Utc};
16use serde::{Deserialize, Serialize};
17
18/// Spotify access token information
19///
20/// [Reference](https://developer.spotify.com/documentation/general/guides/authorization/)
21#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
22pub struct Token {
23    /// An access token that can be provided in subsequent calls
24    pub access_token: String,
25    /// The time period for which the access token is valid.
26    #[serde(with = "duration_second")]
27    pub expires_in: Duration,
28    /// The valid time for which the access token is available represented
29    /// in ISO 8601 combined date and time.
30    pub expires_at: Option<DateTime<Utc>>,
31    /// A token that can be sent to the Spotify Accounts service
32    /// in place of an authorization code
33    pub refresh_token: Option<String>,
34    /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/authorization/scopes/)
35    /// which have been granted for this `access_token`
36    ///
37    /// You may use the `scopes!` macro in
38    /// [`rspotify-macros`](https://docs.rs/rspotify-macros) to build it at
39    /// compile time easily.
40    // The token response from spotify is singular, hence the rename to `scope`
41    #[serde(default, with = "space_separated_scopes", rename = "scope")]
42    pub scopes: HashSet<String>,
43}
44
45impl Default for Token {
46    fn default() -> Self {
47        Self {
48            access_token: String::new(),
49            expires_in: Duration::try_seconds(0).unwrap(),
50            expires_at: Some(Utc::now()),
51            refresh_token: None,
52            scopes: HashSet::new(),
53        }
54    }
55}
56
57impl Token {
58    /// Tries to initialize the token from a cache file.
59    pub fn from_cache<T: AsRef<Path>>(path: T) -> ModelResult<Self> {
60        let mut file = fs::File::open(path)?;
61        let mut tok_str = String::new();
62        file.read_to_string(&mut tok_str)?;
63        let tok = serde_json::from_str(&tok_str)?;
64
65        Ok(tok)
66    }
67
68    /// Saves the token information into its cache file.
69    pub fn write_cache<T: AsRef<Path>>(&self, path: T) -> ModelResult<()> {
70        let token_info = serde_json::to_string(&self)?;
71
72        let mut file = fs::OpenOptions::new().write(true).create(true).open(path)?;
73        file.set_len(0)?;
74        file.write_all(token_info.as_bytes())?;
75
76        Ok(())
77    }
78
79    /// Check if the token is expired. It includes a margin of 10 seconds (which
80    /// is how much a request would take in the worst case scenario).
81    #[must_use]
82    pub fn is_expired(&self) -> bool {
83        self.expires_at.map_or(true, |expiration| {
84            Utc::now() + TimeDelta::try_seconds(10).unwrap() >= expiration
85        })
86    }
87
88    /// Generates an HTTP token authorization header with proper formatting
89    #[must_use]
90    pub fn auth_headers(&self) -> HashMap<String, String> {
91        let auth = "authorization".to_owned();
92        let value = format!("Bearer {}", self.access_token);
93
94        let mut headers = HashMap::new();
95        headers.insert(auth, value);
96        headers
97    }
98}
99
100#[cfg(test)]
101mod test {
102    use std::collections::HashSet;
103
104    use crate::Token;
105    use serde_json::json;
106
107    #[test]
108    fn test_bearer_auth() {
109        let tok = Token {
110            access_token: "access_token".to_string(),
111            ..Default::default()
112        };
113
114        let headers = tok.auth_headers();
115        assert_eq!(headers.len(), 1);
116        assert_eq!(
117            headers.get("authorization"),
118            Some(&"Bearer access_token".to_owned())
119        );
120    }
121
122    #[test]
123    fn test_token_deserialize() {
124        let mut scopes = HashSet::<String>::new();
125        scopes.insert("user-read-email".to_owned());
126        let tok = Token {
127            access_token: "access_token".to_string(),
128            scopes,
129            ..Default::default()
130        };
131        let value = json!(tok);
132        let token = serde_json::from_value::<Token>(value);
133        assert!(token.is_ok());
134        assert_eq!(token.unwrap().scopes, tok.scopes);
135    }
136}