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