1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6use crate::config::Config;
7use crate::{Error, Result};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AuthToken {
11 pub access_token: String,
12 pub refresh_token: String,
13 pub expires_at: DateTime<Utc>,
14 pub account_id: String,
15}
16
17impl AuthToken {
18 pub fn is_expired(&self) -> bool {
19 Utc::now() >= self.expires_at
20 }
21
22 pub fn save(&self) -> Result<()> {
23 let entry = keyring::Entry::new("epik", "auth_token")
25 .map_err(|e| Error::Auth(format!("Failed to access keyring: {}", e)))?;
26
27 let contents = serde_json::to_string(self)?;
28 entry
29 .set_password(&contents)
30 .map_err(|e| Error::Auth(format!("Failed to save to keyring: {}", e)))?;
31
32 let auth_path = Self::auth_path()?;
35 if auth_path.exists() {
36 let _ = fs::remove_file(auth_path);
37 }
38
39 Ok(())
40 }
41
42 pub fn load() -> Result<Option<Self>> {
43 let entry = keyring::Entry::new("epik", "auth_token");
45
46 match entry {
47 Ok(e) => {
48 match e.get_password() {
49 Ok(contents) => {
50 let token: AuthToken = serde_json::from_str(&contents)?;
51 return Ok(Some(token));
52 }
53 Err(_) => {
54 }
56 }
57 }
58 Err(_) => {
59 }
61 }
62
63 let auth_path = Self::auth_path()?;
65
66 if !auth_path.exists() {
67 return Ok(None);
68 }
69
70 let contents = fs::read_to_string(&auth_path)?;
71 let token: AuthToken = serde_json::from_str(&contents)?;
72
73 if let Err(e) = token.save() {
75 eprintln!("Warning: Failed to migrate token to keyring: {}", e);
79 }
80
81 Ok(Some(token))
82 }
83
84 pub fn delete() -> Result<()> {
85 let entry = keyring::Entry::new("epik", "auth_token");
87 if let Ok(e) = entry {
88 let _ = e.delete_password();
89 }
90
91 let auth_path = Self::auth_path()?;
93 if auth_path.exists() {
94 fs::remove_file(&auth_path)?;
95 }
96
97 Ok(())
98 }
99
100 fn auth_path() -> Result<PathBuf> {
101 let data_dir = Config::data_dir()?;
102 Ok(data_dir.join("auth.json"))
103 }
104}
105
106#[derive(Clone)]
107pub struct AuthManager {
108 token: Option<AuthToken>,
109}
110
111impl AuthManager {
112 pub fn new() -> Result<Self> {
113 let token = AuthToken::load()?;
114 Ok(Self { token })
115 }
116
117 pub fn is_authenticated(&self) -> bool {
118 if let Some(token) = &self.token {
119 !token.is_expired()
120 } else {
121 false
122 }
123 }
124
125 pub fn get_token(&self) -> Result<&AuthToken> {
126 match &self.token {
127 Some(token) if !token.is_expired() => Ok(token),
128 _ => Err(Error::NotAuthenticated),
129 }
130 }
131
132 pub fn get_stored_token(&self) -> Option<&AuthToken> {
133 self.token.as_ref()
134 }
135
136 pub fn token_needs_refresh(&self) -> bool {
138 if let Some(token) = &self.token {
139 let now = chrono::Utc::now();
140 let time_until_expiry = token.expires_at.signed_duration_since(now);
141 time_until_expiry.num_minutes() < 5
142 } else {
143 false
144 }
145 }
146
147 pub fn get_refresh_token(&self) -> Option<String> {
148 self.token.as_ref().map(|t| t.refresh_token.clone())
149 }
150
151 pub fn set_token(&mut self, token: AuthToken) -> Result<()> {
152 token.save()?;
153 self.token = Some(token);
154 Ok(())
155 }
156
157 pub fn logout(&mut self) -> Result<()> {
158 AuthToken::delete()?;
159 self.token = None;
160 Ok(())
161 }
162}
163
164impl Default for AuthManager {
165 fn default() -> Self {
166 Self::new().unwrap_or(Self { token: None })
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_auth_manager_not_authenticated_by_default() {
176 let manager = AuthManager { token: None };
177 assert!(!manager.is_authenticated());
178 }
179
180 #[test]
181 fn test_auth_token_expiry() {
182 let expired_token = AuthToken {
183 access_token: "test".to_string(),
184 refresh_token: "test".to_string(),
185 expires_at: Utc::now() - chrono::Duration::hours(1),
186 account_id: "test".to_string(),
187 };
188 assert!(expired_token.is_expired());
189
190 let valid_token = AuthToken {
191 access_token: "test".to_string(),
192 refresh_token: "test".to_string(),
193 expires_at: Utc::now() + chrono::Duration::hours(1),
194 account_id: "test".to_string(),
195 };
196 assert!(!valid_token.is_expired());
197 }
198}