1pub mod jwt;
2
3use serde::{Deserialize, Serialize};
4use std::io::{Read, Write};
5use txtx_core::kit::reqwest;
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct AuthConfig {
10 pub access_token: String,
11 pub exp: u64,
12 pub refresh_token: String,
13 pub pat: Option<String>,
14 pub user: AuthUser,
15}
16
17impl AuthConfig {
18 pub fn new(
19 access_token: String,
20 exp: u64,
21 refresh_token: String,
22 pat: Option<String>,
23 user: AuthUser,
24 ) -> Self {
25 Self { access_token, exp, refresh_token, pat, user }
26 }
27
28 async fn from_refresh_session_response(
29 id_service_url: &str,
30 RefreshSessionResponse { access_token, refresh_token, user }: &RefreshSessionResponse,
31 pat: &Option<String>,
32 ) -> Result<Self, String> {
33 let jwt_manager = jwt::JwtManager::initialize(id_service_url)
34 .await
35 .map_err(|e| format!("Failed to initialize JWT manager: {}", e))?;
36
37 let access_token_claims = jwt_manager
38 .decode_jwt(access_token, true)
39 .map_err(|e| format!("Failed to decode access token: {}", e))?;
40
41 Ok(Self {
42 access_token: access_token.clone(),
43 exp: access_token_claims.exp,
44 refresh_token: refresh_token.clone(),
45 pat: pat.clone(),
46 user: user.clone(),
47 })
48 }
49
50 pub fn write_to_system_config(&self) -> Result<(), String> {
52 let data_dir = dirs::data_dir().ok_or("Failed to get system data directory")?;
53
54 std::fs::create_dir_all(data_dir.join("txtx"))
55 .map_err(|e| format!("Failed to create data directory: {}", e))?;
56
57 let path = data_dir.join("txtx/auth.toml");
58
59 let mut file = std::fs::File::create(&path)
60 .map_err(|e| format!("Failed to create config file: {}", e))?;
61
62 let toml = toml::to_string(&self)
63 .map_err(|e| format!("Failed to serialize auth config: {}", e))?;
64
65 file.write_all(toml.as_bytes())
66 .map_err(|e| format!("Failed to write auth config: {}", e))?;
67 Ok(())
68 }
69
70 pub fn read_from_system_config() -> Result<Option<Self>, String> {
72 let data_dir = dirs::data_dir().ok_or("Failed to get system data directory")?;
73 let path = data_dir.join("txtx/auth.toml");
74
75 if !path.exists() {
76 return Ok(None);
77 }
78
79 let mut file =
80 std::fs::File::open(&path).map_err(|e| format!("Failed to open config file: {}", e))?;
81 let mut buf = String::new();
82
83 file.read_to_string(&mut buf).map_err(|e| format!("Failed to read config file: {}", e))?;
84
85 let config =
86 toml::from_str(&buf).map_err(|e| format!("Failed to parse auth config file: {}", e))?;
87 Ok(Some(config))
88 }
89
90 pub async fn refresh_session_if_needed(&mut self, id_service_url: &str) -> Result<(), String> {
91 if self.is_access_token_expired() {
92 let refreshed_auth_config = self.get_refreshed_session(id_service_url).await.map_err(|e| {
93 format!("Failed to refresh session. Run `txtx cloud login` to log in again. Downstream error: {e}")
94 })?;
95 self.access_token = refreshed_auth_config.access_token;
96 self.exp = refreshed_auth_config.exp;
97 self.refresh_token = refreshed_auth_config.refresh_token;
98 self.write_to_system_config()
99 .map_err(|e| format!("Failed to write refreshed session to config: {}", e))?;
100 return Ok(());
101 }
102 Ok(())
103 }
104
105 async fn get_refreshed_session(&self, id_service_url: &str) -> Result<AuthConfig, String> {
108 let client = reqwest::Client::new();
109 let res = client
110 .post(&format!("{id_service_url}/token"))
111 .json(&serde_json::json!({
112 "refreshToken": &self.refresh_token,
113 }))
114 .send()
115 .await
116 .map_err(|e| format!("Failed to send request to refresh session: {}", e))?;
117
118 if res.status().is_success() {
119 let res = res
120 .json::<RefreshSessionResponse>()
121 .await
122 .map_err(|e| format!("Failed to parse response: {}", e))?;
123
124 let auth_config =
125 AuthConfig::from_refresh_session_response(id_service_url, &res, &self.pat)
126 .await
127 .map_err(|e| format!("Failed to parse refresh session response: {e}"))?;
128
129 auth_config
130 .write_to_system_config()
131 .map_err(|e| format!("Failed to write refreshed session to config: {}", e))?;
132 return Ok(auth_config);
133 } else {
134 let err = res.text().await.unwrap_or_else(|_| "Unknown error".to_string());
135 return Err(format!("Received error from refresh session request: {}", err));
136 }
137 }
138
139 pub fn is_access_token_expired(&self) -> bool {
140 let now = std::time::SystemTime::now()
141 .duration_since(std::time::UNIX_EPOCH)
142 .expect("SystemTime before UNIX EPOCH")
143 .as_secs() as i64;
144
145 self.exp < now as u64
146 }
147}
148
149#[derive(Debug, Clone, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct RefreshSessionResponse {
152 pub access_token: String,
153 pub refresh_token: String,
154 pub user: AuthUser,
155}
156
157#[derive(Debug, Clone, Deserialize, Serialize)]
158#[serde(rename_all = "camelCase")]
159pub struct AuthUser {
160 pub id: String,
161 pub email: Option<String>,
162 pub display_name: String,
163}