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