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