Skip to main content

trailcache_core/auth/
session.rs

1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7/// Session file name in cache directory
8const SESSION_FILE: &str = "session.json";
9
10/// Token expiry time in minutes.
11/// Scouting.org tokens expire after ~30 minutes of inactivity.
12const TOKEN_EXPIRY_MINUTES: i64 = 30;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SessionData {
16    pub token: String,
17    pub user_id: i64,
18    pub person_guid: String,
19    pub organization_guid: String,
20    pub username: String,
21    pub created_at: DateTime<Utc>,
22}
23
24/// Buffer time before expiry to trigger refresh (5 minutes)
25const TOKEN_REFRESH_BUFFER_MINUTES: i64 = 5;
26
27impl SessionData {
28    pub fn is_expired(&self) -> bool {
29        let expiry = self.created_at + Duration::minutes(TOKEN_EXPIRY_MINUTES);
30        Utc::now() > expiry
31    }
32
33    /// Check if the session will expire soon and should be refreshed
34    #[allow(dead_code)]
35    pub fn needs_refresh(&self) -> bool {
36        let refresh_at = self.created_at + Duration::minutes(TOKEN_EXPIRY_MINUTES - TOKEN_REFRESH_BUFFER_MINUTES);
37        Utc::now() > refresh_at
38    }
39
40    #[allow(dead_code)]
41    pub fn time_until_expiry(&self) -> Duration {
42        let expiry = self.created_at + Duration::minutes(TOKEN_EXPIRY_MINUTES);
43        expiry - Utc::now()
44    }
45
46    /// Get minutes remaining until expiry (for display)
47    #[allow(dead_code)]
48    pub fn minutes_until_expiry(&self) -> i64 {
49        self.time_until_expiry().num_minutes().max(0)
50    }
51}
52
53pub struct Session {
54    cache_dir: PathBuf,
55    pub data: Option<SessionData>,
56}
57
58impl Session {
59    pub fn new(cache_dir: PathBuf) -> Self {
60        Self {
61            cache_dir,
62            data: None,
63        }
64    }
65
66    /// Load session from disk
67    pub fn load(&mut self) -> Result<bool> {
68        let path = self.session_path();
69        if path.exists() {
70            let contents = std::fs::read_to_string(&path)
71                .context("Failed to read session file")?;
72            let data: SessionData = serde_json::from_str(&contents)
73                .context("Failed to parse session file")?;
74
75            if !data.is_expired() {
76                self.data = Some(data);
77                return Ok(true);
78            }
79        }
80        Ok(false)
81    }
82
83    /// Save session to disk
84    pub fn save(&self) -> Result<()> {
85        if let Some(ref data) = self.data {
86            let path = self.session_path();
87            if let Some(parent) = path.parent() {
88                std::fs::create_dir_all(parent)?;
89            }
90            let contents = serde_json::to_string_pretty(data)?;
91            std::fs::write(path, contents)?;
92        }
93        Ok(())
94    }
95
96    /// Clear session data
97    #[allow(dead_code)]
98    pub fn clear(&mut self) -> Result<()> {
99        self.data = None;
100        let path = self.session_path();
101        if path.exists() {
102            std::fs::remove_file(path)?;
103        }
104        Ok(())
105    }
106
107    /// Update session with new data
108    pub fn update(&mut self, data: SessionData) {
109        self.data = Some(data);
110    }
111
112    /// Get the bearer token if session is valid
113    pub fn token(&self) -> Option<&str> {
114        self.data.as_ref().map(|d| d.token.as_str())
115    }
116
117    /// Get the user ID if session exists
118    pub fn user_id(&self) -> Option<i64> {
119        self.data.as_ref().map(|d| d.user_id)
120    }
121
122    /// Check if session is valid (exists and not expired)
123    #[allow(dead_code)]
124    pub fn is_valid(&self) -> bool {
125        self.data.as_ref().map(|d| !d.is_expired()).unwrap_or(false)
126    }
127
128    fn session_path(&self) -> PathBuf {
129        self.cache_dir.join(SESSION_FILE)
130    }
131}