1use crate::error::SessionResult;
2use crate::error::{GreenticError, invalid_argument, not_found};
3use crate::store::SessionStore;
4use greentic_types::{EnvId, SessionData, SessionKey, TeamId, TenantCtx, TenantId, UserId};
5use parking_lot::RwLock;
6use std::collections::HashMap;
7use uuid::Uuid;
8
9pub struct InMemorySessionStore {
11 sessions: RwLock<HashMap<SessionKey, SessionData>>,
12 user_index: RwLock<HashMap<UserLookupKey, SessionKey>>,
13}
14
15impl Default for InMemorySessionStore {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl InMemorySessionStore {
22 pub fn new() -> Self {
24 Self {
25 sessions: RwLock::new(HashMap::new()),
26 user_index: RwLock::new(HashMap::new()),
27 }
28 }
29
30 fn next_key() -> SessionKey {
31 SessionKey::new(Uuid::new_v4().to_string())
32 }
33
34 fn normalize_team(ctx: &TenantCtx) -> Option<&TeamId> {
35 ctx.team_id.as_ref().or(ctx.team.as_ref())
36 }
37
38 fn normalize_user(ctx: &TenantCtx) -> Option<&UserId> {
39 ctx.user_id.as_ref().or(ctx.user.as_ref())
40 }
41
42 fn ctx_mismatch(expected: &TenantCtx, provided: &TenantCtx, reason: &str) -> GreenticError {
43 let expected_team = Self::normalize_team(expected)
44 .map(|t| t.as_str())
45 .unwrap_or("-");
46 let provided_team = Self::normalize_team(provided)
47 .map(|t| t.as_str())
48 .unwrap_or("-");
49 let expected_user = Self::normalize_user(expected)
50 .map(|u| u.as_str())
51 .unwrap_or("-");
52 let provided_user = Self::normalize_user(provided)
53 .map(|u| u.as_str())
54 .unwrap_or("-");
55 invalid_argument(format!(
56 "tenant context mismatch ({reason}): expected env={}, tenant={}, team={}, user={}, got env={}, tenant={}, team={}, user={}",
57 expected.env.as_str(),
58 expected.tenant_id.as_str(),
59 expected_team,
60 expected_user,
61 provided.env.as_str(),
62 provided.tenant_id.as_str(),
63 provided_team,
64 provided_user
65 ))
66 }
67
68 fn ensure_alignment(ctx: &TenantCtx, data: &SessionData) -> SessionResult<()> {
69 let stored = &data.tenant_ctx;
70 if ctx.env != stored.env || ctx.tenant_id != stored.tenant_id {
71 return Err(Self::ctx_mismatch(stored, ctx, "env/tenant must match"));
72 }
73 if Self::normalize_team(ctx) != Self::normalize_team(stored) {
74 return Err(Self::ctx_mismatch(stored, ctx, "team must match"));
75 }
76 if let Some(stored_user) = Self::normalize_user(stored) {
77 let Some(provided_user) = Self::normalize_user(ctx) else {
78 return Err(Self::ctx_mismatch(
79 stored,
80 ctx,
81 "user required by session but missing in caller context",
82 ));
83 };
84 if stored_user != provided_user {
85 return Err(Self::ctx_mismatch(
86 stored,
87 ctx,
88 "user must match stored session",
89 ));
90 }
91 }
92 Ok(())
93 }
94
95 fn ensure_ctx_preserved(existing: &TenantCtx, candidate: &TenantCtx) -> SessionResult<()> {
96 if existing.env != candidate.env || existing.tenant_id != candidate.tenant_id {
97 return Err(Self::ctx_mismatch(
98 existing,
99 candidate,
100 "env/tenant cannot change for an existing session",
101 ));
102 }
103 if Self::normalize_team(existing) != Self::normalize_team(candidate) {
104 return Err(Self::ctx_mismatch(
105 existing,
106 candidate,
107 "team cannot change for an existing session",
108 ));
109 }
110 match (
111 Self::normalize_user(existing),
112 Self::normalize_user(candidate),
113 ) {
114 (Some(a), Some(b)) if a == b => {}
115 (Some(_), Some(_)) | (Some(_), None) => {
116 return Err(Self::ctx_mismatch(
117 existing,
118 candidate,
119 "user cannot change for an existing session",
120 ));
121 }
122 (None, Some(_)) => {
123 return Err(Self::ctx_mismatch(
124 existing,
125 candidate,
126 "user cannot be introduced when none was stored",
127 ));
128 }
129 (None, None) => {}
130 }
131 Ok(())
132 }
133
134 fn lookup_from_ctx(ctx: &TenantCtx) -> Option<UserLookupKey> {
135 let user = ctx.user_id.clone().or_else(|| ctx.user.clone())?;
136 Some(UserLookupKey::from_ctx(ctx, &user))
137 }
138
139 fn lookup_from_data(data: &SessionData) -> Option<UserLookupKey> {
140 let user = data
141 .tenant_ctx
142 .user_id
143 .clone()
144 .or_else(|| data.tenant_ctx.user.clone())?;
145 Some(UserLookupKey::from_ctx(&data.tenant_ctx, &user))
146 }
147
148 fn record_user_mapping(
149 &self,
150 ctx_hint: Option<&TenantCtx>,
151 data: &SessionData,
152 key: &SessionKey,
153 ) {
154 let lookup =
155 Self::lookup_from_data(data).or_else(|| ctx_hint.and_then(Self::lookup_from_ctx));
156 if let Some(entry) = lookup {
157 self.user_index.write().insert(entry, key.clone());
158 }
159 }
160
161 fn purge_user_mapping(&self, data: &SessionData, key: &SessionKey) {
162 if let Some(entry) = Self::lookup_from_data(data) {
163 let mut guard = self.user_index.write();
164 if guard
165 .get(&entry)
166 .map(|existing| existing == key)
167 .unwrap_or(false)
168 {
169 guard.remove(&entry);
170 }
171 }
172 }
173}
174
175impl SessionStore for InMemorySessionStore {
176 fn create_session(&self, ctx: &TenantCtx, data: SessionData) -> SessionResult<SessionKey> {
177 Self::ensure_alignment(ctx, &data)?;
178 let key = Self::next_key();
179 self.sessions.write().insert(key.clone(), data.clone());
180 self.record_user_mapping(Some(ctx), &data, &key);
181 Ok(key)
182 }
183
184 fn get_session(&self, key: &SessionKey) -> SessionResult<Option<SessionData>> {
185 Ok(self.sessions.read().get(key).cloned())
186 }
187
188 fn update_session(&self, key: &SessionKey, data: SessionData) -> SessionResult<()> {
189 let mut sessions = self.sessions.write();
190 let Some(previous) = sessions.get(key).cloned() else {
191 return Err(not_found(key));
192 };
193 Self::ensure_ctx_preserved(&previous.tenant_ctx, &data.tenant_ctx)?;
194 sessions.insert(key.clone(), data.clone());
195 drop(sessions);
196 self.purge_user_mapping(&previous, key);
197 self.record_user_mapping(None, &data, key);
198 Ok(())
199 }
200
201 fn remove_session(&self, key: &SessionKey) -> SessionResult<()> {
202 if let Some(old) = self.sessions.write().remove(key) {
203 self.purge_user_mapping(&old, key);
204 Ok(())
205 } else {
206 Err(not_found(key))
207 }
208 }
209
210 fn find_by_user(
211 &self,
212 ctx: &TenantCtx,
213 user: &UserId,
214 ) -> SessionResult<Option<(SessionKey, SessionData)>> {
215 let lookup = UserLookupKey::from_ctx(ctx, user);
216 if let Some(stored_key) = self.user_index.read().get(&lookup).cloned() {
217 if let Some(data) = self.sessions.read().get(&stored_key).cloned() {
218 let stored_ctx = &data.tenant_ctx;
219 if stored_ctx.env == ctx.env
220 && stored_ctx.tenant_id == ctx.tenant_id
221 && Self::normalize_team(stored_ctx) == Self::normalize_team(ctx)
222 {
223 if let Some(stored_user) = Self::normalize_user(stored_ctx)
224 && stored_user != user
225 {
226 self.user_index.write().remove(&lookup);
227 return Ok(None);
228 }
229 return Ok(Some((stored_key, data)));
230 }
231 self.user_index.write().remove(&lookup);
232 }
233 self.user_index.write().remove(&lookup);
234 }
235 Ok(None)
236 }
237}
238
239#[derive(Clone, Eq, PartialEq, Hash)]
240struct UserLookupKey {
241 env: EnvId,
242 tenant: TenantId,
243 team: Option<TeamId>,
244 user: UserId,
245}
246
247impl UserLookupKey {
248 fn from_ctx(ctx: &TenantCtx, user: &UserId) -> Self {
249 Self {
250 env: ctx.env.clone(),
251 tenant: ctx.tenant_id.clone(),
252 team: ctx.team_id.clone().or_else(|| ctx.team.clone()),
253 user: user.clone(),
254 }
255 }
256}