systemprompt_cloud/cli_session/
session.rs1use std::fs;
12use std::path::{Path, PathBuf};
13
14use chrono::{DateTime, Duration, Utc};
15use serde::{Deserialize, Serialize};
16use systemprompt_identifiers::{
17 ContextId, Email, ProfileName, SessionId, SessionToken, TenantId, UserId,
18};
19use systemprompt_models::auth::UserType;
20
21use super::{LOCAL_SESSION_KEY, SessionKey};
22use crate::error::{CloudError, CloudResult};
23
24const CURRENT_VERSION: u32 = 5;
25const MIN_SUPPORTED_VERSION: u32 = 5;
26const SESSION_DURATION_HOURS: i64 = 24;
27
28#[derive(Debug, Clone)]
31pub struct SessionIdentity {
32 pub user_id: UserId,
33 pub email: Email,
34 pub user_type: UserType,
35}
36
37impl SessionIdentity {
38 #[must_use]
39 pub const fn new(user_id: UserId, email: Email, user_type: UserType) -> Self {
40 Self {
41 user_id,
42 email,
43 user_type,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct CliSession {
50 pub version: u32,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub tenant_key: Option<TenantId>,
53 pub profile_name: ProfileName,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub profile_path: Option<PathBuf>,
56 pub session_token: SessionToken,
57 pub session_id: SessionId,
58 pub context_id: ContextId,
59 pub user_id: UserId,
60 pub user_email: Email,
61 pub user_type: UserType,
62 pub created_at: DateTime<Utc>,
63 pub expires_at: DateTime<Utc>,
64 pub last_used: DateTime<Utc>,
65}
66
67#[derive(Debug)]
68pub struct CliSessionBuilder {
69 tenant_key: Option<TenantId>,
70 profile_name: ProfileName,
71 profile_path: Option<PathBuf>,
72 session_token: SessionToken,
73 session_id: SessionId,
74 context_id: ContextId,
75 user_id: UserId,
76 user_email: Email,
77 user_type: UserType,
78}
79
80impl CliSessionBuilder {
81 pub fn new(
82 profile_name: ProfileName,
83 session_token: SessionToken,
84 session_id: SessionId,
85 context_id: ContextId,
86 identity: SessionIdentity,
87 ) -> Self {
88 Self {
89 tenant_key: None,
90 profile_name,
91 profile_path: None,
92 session_token,
93 session_id,
94 context_id,
95 user_id: identity.user_id,
96 user_email: identity.email,
97 user_type: identity.user_type,
98 }
99 }
100
101 #[must_use]
102 pub fn with_tenant_key(mut self, tenant_key: TenantId) -> Self {
103 self.tenant_key = Some(tenant_key);
104 self
105 }
106
107 #[must_use]
108 pub fn with_session_key(mut self, key: &SessionKey) -> Self {
109 self.tenant_key = match key {
110 SessionKey::Local => Some(TenantId::new(LOCAL_SESSION_KEY)),
111 SessionKey::Tenant(id) => Some(id.clone()),
112 };
113 self
114 }
115
116 #[must_use]
117 pub fn with_profile_path(mut self, profile_path: impl Into<PathBuf>) -> Self {
118 self.profile_path = Some(profile_path.into());
119 self
120 }
121
122 #[must_use]
123 pub fn build(self) -> CliSession {
124 let now = Utc::now();
125 let expires_at = now + Duration::hours(SESSION_DURATION_HOURS);
126 CliSession {
127 version: CURRENT_VERSION,
128 tenant_key: self.tenant_key,
129 profile_name: self.profile_name,
130 profile_path: self.profile_path,
131 session_token: self.session_token,
132 session_id: self.session_id,
133 context_id: self.context_id,
134 user_id: self.user_id,
135 user_email: self.user_email,
136 user_type: self.user_type,
137 created_at: now,
138 expires_at,
139 last_used: now,
140 }
141 }
142}
143
144impl CliSession {
145 pub fn builder(
146 profile_name: ProfileName,
147 session_token: SessionToken,
148 session_id: SessionId,
149 context_id: ContextId,
150 identity: SessionIdentity,
151 ) -> CliSessionBuilder {
152 CliSessionBuilder::new(
153 profile_name,
154 session_token,
155 session_id,
156 context_id,
157 identity,
158 )
159 }
160
161 pub const fn context_id(&self) -> &ContextId {
162 &self.context_id
163 }
164
165 pub fn touch(&mut self) {
166 self.last_used = Utc::now();
167 }
168
169 pub fn set_context_id(&mut self, context_id: ContextId) {
170 self.context_id = context_id;
171 self.last_used = Utc::now();
172 }
173
174 pub fn update_profile_path(&mut self, profile_path: PathBuf) {
175 self.profile_path = Some(profile_path);
176 self.last_used = Utc::now();
177 }
178
179 #[must_use]
180 pub fn is_expired(&self) -> bool {
181 Utc::now() >= self.expires_at
182 }
183
184 #[must_use]
185 pub fn is_valid_for_profile(&self, profile_name: &str) -> bool {
186 self.profile_name.as_str() == profile_name && !self.is_expired()
187 }
188
189 #[must_use]
190 pub fn has_valid_credentials(&self) -> bool {
191 !self.session_token.as_str().is_empty()
192 }
193
194 #[must_use]
195 pub fn is_valid_for_tenant(&self, key: &SessionKey) -> bool {
196 if self.is_expired() || !self.has_valid_credentials() {
197 return false;
198 }
199
200 match (key, &self.tenant_key) {
201 (SessionKey::Local, None) => true,
202 (SessionKey::Local, Some(k)) => k.as_str() == LOCAL_SESSION_KEY,
203 (SessionKey::Tenant(id), Some(k)) => k == id,
204 (SessionKey::Tenant(_), None) => false,
205 }
206 }
207
208 #[must_use]
209 pub fn session_key(&self) -> SessionKey {
210 match &self.tenant_key {
211 None => SessionKey::Local,
212 Some(k) if k.as_str() == LOCAL_SESSION_KEY => SessionKey::Local,
213 Some(k) => SessionKey::Tenant(k.clone()),
214 }
215 }
216
217 pub fn load_from_path(path: &Path) -> CloudResult<Self> {
218 if !path.exists() {
219 return Err(CloudError::NotAuthenticated);
220 }
221
222 let content = fs::read_to_string(path)?;
223
224 let mut session: Self = serde_json::from_str(&content)
225 .map_err(|e| CloudError::CredentialsCorrupted { source: e })?;
226
227 if session.version < MIN_SUPPORTED_VERSION || session.version > CURRENT_VERSION {
228 return Err(CloudError::SessionVersionMismatch {
229 min: MIN_SUPPORTED_VERSION,
230 max: CURRENT_VERSION,
231 actual: session.version,
232 path: path.display().to_string(),
233 });
234 }
235
236 session.version = CURRENT_VERSION;
237 Ok(session)
238 }
239
240 pub fn save_to_path(&self, path: &Path) -> CloudResult<()> {
241 if let Some(dir) = path.parent() {
242 fs::create_dir_all(dir)?;
243
244 let gitignore_path = dir.join(".gitignore");
245 if !gitignore_path.exists() {
246 fs::write(&gitignore_path, "*\n")?;
247 }
248 }
249
250 let content = serde_json::to_string_pretty(self)?;
251 fs::write(path, content)?;
252
253 #[cfg(unix)]
254 {
255 use std::os::unix::fs::PermissionsExt;
256 let mut perms = fs::metadata(path)?.permissions();
257 perms.set_mode(0o600);
258 fs::set_permissions(path, perms)?;
259 }
260
261 Ok(())
262 }
263
264 pub fn delete_from_path(path: &Path) -> CloudResult<()> {
265 if path.exists() {
266 fs::remove_file(path)?;
267 }
268 Ok(())
269 }
270}