systemprompt_cloud/cli_session/
session.rs1use std::path::PathBuf;
12
13use chrono::{DateTime, Duration, Utc};
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{
16 ContextId, Email, ProfileName, SessionId, SessionToken, TenantId, UserId,
17};
18use systemprompt_models::auth::UserType;
19
20use super::{LOCAL_SESSION_KEY, SessionKey};
21
22pub(super) const CURRENT_VERSION: u32 = 6;
23pub(super) const MIN_SUPPORTED_VERSION: u32 = 6;
24const SESSION_DURATION_HOURS: i64 = 24;
25
26#[derive(Debug, Clone)]
30pub struct SessionBinding {
31 pub profile_name: ProfileName,
32 pub issuer: String,
33}
34
35impl SessionBinding {
36 #[must_use]
37 pub const fn new(profile_name: ProfileName, issuer: String) -> Self {
38 Self {
39 profile_name,
40 issuer,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
46pub struct SessionIdentity {
47 pub user_id: UserId,
48 pub email: Email,
49 pub user_type: UserType,
50}
51
52impl SessionIdentity {
53 #[must_use]
54 pub const fn new(user_id: UserId, email: Email, user_type: UserType) -> Self {
55 Self {
56 user_id,
57 email,
58 user_type,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CliSession {
65 pub version: u32,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub tenant_key: Option<TenantId>,
68 pub profile_name: ProfileName,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub profile_path: Option<PathBuf>,
71 pub session_token: SessionToken,
72 pub issuer: String,
73 pub session_id: SessionId,
74 pub context_id: ContextId,
75 pub user_id: UserId,
76 pub user_email: Email,
77 pub user_type: UserType,
78 #[serde(default = "Utc::now")]
79 pub created_at: DateTime<Utc>,
80 pub expires_at: DateTime<Utc>,
81 #[serde(default = "Utc::now")]
82 pub last_used: DateTime<Utc>,
83}
84
85#[derive(Debug)]
86pub struct CliSessionBuilder {
87 tenant_key: Option<TenantId>,
88 profile_name: ProfileName,
89 profile_path: Option<PathBuf>,
90 session_token: SessionToken,
91 issuer: String,
92 session_id: SessionId,
93 context_id: ContextId,
94 user_id: UserId,
95 user_email: Email,
96 user_type: UserType,
97 ttl: Duration,
98}
99
100impl CliSessionBuilder {
101 pub fn new(
102 binding: SessionBinding,
103 session_token: SessionToken,
104 session_id: SessionId,
105 context_id: ContextId,
106 identity: SessionIdentity,
107 ) -> Self {
108 Self {
109 tenant_key: None,
110 profile_name: binding.profile_name,
111 profile_path: None,
112 session_token,
113 issuer: binding.issuer,
114 ttl: Duration::hours(SESSION_DURATION_HOURS),
115 session_id,
116 context_id,
117 user_id: identity.user_id,
118 user_email: identity.email,
119 user_type: identity.user_type,
120 }
121 }
122
123 #[must_use]
124 pub fn with_tenant_key(mut self, tenant_key: TenantId) -> Self {
125 self.tenant_key = Some(tenant_key);
126 self
127 }
128
129 #[must_use]
130 pub fn with_session_key(mut self, key: &SessionKey) -> Self {
131 self.tenant_key = match key {
132 SessionKey::Local => Some(TenantId::new(LOCAL_SESSION_KEY)),
133 SessionKey::Tenant(id) => Some(id.clone()),
134 };
135 self
136 }
137
138 #[must_use]
139 pub fn with_profile_path(mut self, profile_path: impl Into<PathBuf>) -> Self {
140 self.profile_path = Some(profile_path.into());
141 self
142 }
143
144 #[must_use]
145 pub const fn with_ttl(mut self, ttl: Duration) -> Self {
146 self.ttl = ttl;
147 self
148 }
149
150 #[must_use]
151 pub fn build(self) -> CliSession {
152 let now = Utc::now();
153 let expires_at = now + self.ttl;
154 CliSession {
155 version: CURRENT_VERSION,
156 tenant_key: self.tenant_key,
157 profile_name: self.profile_name,
158 profile_path: self.profile_path,
159 session_token: self.session_token,
160 issuer: self.issuer,
161 session_id: self.session_id,
162 context_id: self.context_id,
163 user_id: self.user_id,
164 user_email: self.user_email,
165 user_type: self.user_type,
166 created_at: now,
167 expires_at,
168 last_used: now,
169 }
170 }
171}
172
173impl CliSession {
174 pub fn builder(
175 binding: SessionBinding,
176 session_token: SessionToken,
177 session_id: SessionId,
178 context_id: ContextId,
179 identity: SessionIdentity,
180 ) -> CliSessionBuilder {
181 CliSessionBuilder::new(binding, session_token, session_id, context_id, identity)
182 }
183
184 #[must_use]
185 pub fn matches_issuer(&self, issuer: &str) -> bool {
186 self.issuer == issuer
187 }
188
189 pub const fn context_id(&self) -> &ContextId {
190 &self.context_id
191 }
192
193 pub fn touch(&mut self) {
194 self.last_used = Utc::now();
195 }
196
197 pub fn set_context_id(&mut self, context_id: ContextId) {
198 self.context_id = context_id;
199 self.last_used = Utc::now();
200 }
201
202 pub fn update_profile_path(&mut self, profile_path: PathBuf) {
203 self.profile_path = Some(profile_path);
204 self.last_used = Utc::now();
205 }
206
207 #[must_use]
208 pub fn is_expired(&self) -> bool {
209 Utc::now() >= self.expires_at
210 }
211
212 #[must_use]
213 pub fn is_valid_for_profile(&self, profile_name: &str) -> bool {
214 self.profile_name.as_str() == profile_name && !self.is_expired()
215 }
216
217 #[must_use]
218 pub fn has_valid_credentials(&self) -> bool {
219 !self.session_token.as_str().is_empty()
220 }
221
222 #[must_use]
223 pub fn is_valid_for_tenant(&self, key: &SessionKey) -> bool {
224 if self.is_expired() || !self.has_valid_credentials() {
225 return false;
226 }
227
228 match (key, &self.tenant_key) {
229 (SessionKey::Local, None) => true,
230 (SessionKey::Local, Some(k)) => k.as_str() == LOCAL_SESSION_KEY,
231 (SessionKey::Tenant(id), Some(k)) => k == id,
232 (SessionKey::Tenant(_), None) => false,
233 }
234 }
235
236 #[must_use]
237 pub fn session_key(&self) -> SessionKey {
238 match &self.tenant_key {
239 None => SessionKey::Local,
240 Some(k) if k.as_str() == LOCAL_SESSION_KEY => SessionKey::Local,
241 Some(k) => SessionKey::Tenant(k.clone()),
242 }
243 }
244}