trailcache_core/auth/
session.rs1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6
7const SESSION_FILE: &str = "session.json";
9
10const 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
24const 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 #[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 #[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 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 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 #[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 pub fn update(&mut self, data: SessionData) {
109 self.data = Some(data);
110 }
111
112 pub fn token(&self) -> Option<&str> {
114 self.data.as_ref().map(|d| d.token.as_str())
115 }
116
117 pub fn user_id(&self) -> Option<i64> {
119 self.data.as_ref().map(|d| d.user_id)
120 }
121
122 #[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}