1#[cfg(feature = "session-screens")]
2mod screen;
3
4use std::time::Duration;
5
6use sofie::core::Instant;
7use sofie::db::{Db, Uuid};
8use sofie::http::SessionRecord;
9use sofie::prelude::*;
10
11#[cfg(feature = "session-screens")]
12pub use screen::routes;
13
14use crate::AccountId;
15
16const DEVICE_CHARACTERS: usize = 200;
18const LAST_SEEN_PRECISION: Duration = Duration::from_secs(60);
20pub(crate) const PRUNE_EVERY: Duration = Duration::from_hours(1);
21const PRUNE_MARGIN: Duration = Duration::from_hours(1);
24#[cfg(any(feature = "passkeys", feature = "oauth", feature = "totp"))]
25const RECENT_SIGN_IN: Duration = Duration::from_mins(10);
26
27#[derive(Aggregate)]
29pub struct AccountSession {
30 meta: Meta,
31 #[owner]
32 account: AccountId,
33 session: Uuid,
34 area: String,
35 principal: Option<Uuid>,
37 device: Option<String>,
38 last_seen_at: Instant,
39 ended_at: Option<Instant>,
40 expires_at: Option<Instant>,
42}
43
44impl AccountSession {
45 #[must_use]
46 pub fn area(&self) -> &str {
47 &self.area
48 }
49
50 #[must_use]
51 pub fn device(&self) -> Option<&str> {
52 self.device.as_deref()
53 }
54
55 #[must_use]
56 pub fn last_seen_at(&self) -> Instant {
57 self.last_seen_at
58 }
59
60 fn end(&mut self, now: Instant) {
61 self.ended_at.get_or_insert(now);
62 }
63
64 #[cfg(feature = "impersonation")]
65 pub(crate) fn impersonated(&mut self, by: &Email, until: Instant) {
66 self.device = Some(format!("Impersonated by {}", by.as_str()));
67 self.expires_at = Some(until);
68 }
69}
70
71pub(crate) fn started(
72 session: Uuid,
73 account: AccountId,
74 area: &str,
75 principal: Uuid,
76 user_agent: Option<&str>,
77 now: Instant,
78) -> Result<AccountSession> {
79 Ok(AccountSession {
80 meta: Meta::new()?,
81 account,
82 session,
83 area: area.to_owned(),
84 principal: Some(principal),
85 device: user_agent.map(device),
86 last_seen_at: now,
87 ended_at: None,
88 expires_at: None,
89 })
90}
91
92fn device(user_agent: &str) -> String {
93 user_agent
94 .chars()
95 .filter(|character| !sofie::core::misleads_display(*character))
96 .take(DEVICE_CHARACTERS)
97 .collect()
98}
99
100pub(crate) async fn record(
102 db: &Db,
103 session: Uuid,
104 seen: Option<sofie::core::Timestamp>,
105) -> Result<SessionRecord> {
106 let read = db.read();
107 let Some(mut record) = recorded(&read, session).await? else {
108 return Ok(SessionRecord::Unrecorded);
109 };
110 if record.ended_at.is_some() {
111 return Ok(SessionRecord::Ended);
112 }
113 #[cfg(feature = "impersonation")]
115 let impersonator = match record.expires_at {
116 Some(_) => match crate::impersonation::checked(&read, session).await? {
117 crate::impersonation::Checked::Absent => None,
118 crate::impersonation::Checked::By(impersonator) => Some(impersonator),
119 crate::impersonation::Checked::Beyond(impersonation) => {
120 return ending_impersonated(db, record, *impersonation).await;
121 }
122 },
123 None => None,
124 };
125 #[cfg(not(feature = "impersonation"))]
126 let impersonator = None;
127 let held = SessionRecord::Live {
128 account: record.account.uuid(),
129 expires_at: record.expires_at,
130 impersonator,
131 };
132 let Some(seen) = seen.map(Instant::from) else {
133 return Ok(held);
134 };
135 if !held.is_live(seen) || seen < record.last_seen_at.checked_add(LAST_SEEN_PRECISION)? {
136 return Ok(held);
137 }
138 record.last_seen_at = seen;
139 match db.commit(&mut record).await {
140 Err(Error::Conflict) | Ok(()) => Ok(held),
142 Err(error) => Err(error),
143 }
144}
145
146#[cfg(feature = "impersonation")]
147async fn ending_impersonated(
148 db: &Db,
149 mut record: AccountSession,
150 mut impersonation: crate::impersonation::Impersonation,
151) -> Result<SessionRecord> {
152 let now = db.clock().now();
153 impersonation.end(now)?;
154 record.end(now);
155 match db.commit(&mut (record, impersonation)).await {
156 Err(Error::Conflict) | Ok(()) => Ok(SessionRecord::Ended),
158 Err(error) => Err(error),
159 }
160}
161
162pub(crate) async fn prune(prune: sofie::db::Prune, clock: Clock) -> Result<()> {
164 let kept = sofie::auth::SessionClaims::ABSOLUTE_LIFETIME + PRUNE_MARGIN;
165 let before = clock.now().checked_sub(kept)?;
166 prune
167 .before(AccountSession::LAST_SEEN_AT, before)
168 .await
169 .map(drop)
170}
171
172pub async fn account_of(read: &Read, session: Uuid) -> Result<Option<AccountId>> {
174 let record = recorded(read, session).await?;
175 Ok(record
176 .filter(|record| record.ended_at.is_none())
177 .map(|record| record.account))
178}
179
180async fn recorded(read: &Read, session: Uuid) -> Result<Option<AccountSession>> {
181 AccountSession::query()
182 .where_(AccountSession::SESSION.eq(session))
183 .first(read)
184 .await
185}
186
187#[cfg(any(feature = "passkeys", feature = "oauth", feature = "totp"))]
188pub(crate) async fn signed_in_recently(read: &Read, session: Uuid, now: Instant) -> Result<bool> {
189 AccountSession::query()
190 .where_(AccountSession::SESSION.eq(session))
191 .where_(AccountSession::ENDED_AT.is_null())
192 .where_(AccountSession::CREATED_AT.gt(now.checked_sub(RECENT_SIGN_IN)?))
193 .exists(read)
194 .await
195}
196
197pub(crate) async fn ending_current(
198 read: &Read,
199 session: &Session,
200 clock: &Clock,
201) -> Result<Vec<AccountSession>> {
202 let Some(current) = session.current() else {
203 return Ok(Vec::new());
204 };
205 let mut ended: Vec<AccountSession> = recorded(read, current).await?.into_iter().collect();
206 for record in &mut ended {
207 record.end(clock.now());
208 }
209 Ok(ended)
210}
211
212#[derive(Default)]
214pub(crate) struct Impersonations {
215 #[cfg(feature = "impersonation")]
216 ended: Vec<crate::impersonation::Impersonation>,
217}
218
219impl sofie::db::Outcome for Impersonations {
220 fn plan(&self, plan: &mut sofie::db::Plan<'_>) -> Result<()> {
221 #[cfg(feature = "impersonation")]
222 self.ended.plan(plan)?;
223 #[cfg(not(feature = "impersonation"))]
224 let _ = plan;
225 Ok(())
226 }
227
228 fn committed(&mut self, stamp: sofie::db::Stamp) {
229 #[cfg(feature = "impersonation")]
230 self.ended.committed(stamp);
231 #[cfg(not(feature = "impersonation"))]
232 let _ = stamp;
233 }
234}
235
236pub(crate) async fn impersonations_in(
237 read: &Read,
238 ended: &[AccountSession],
239 now: Instant,
240) -> Result<Impersonations> {
241 #[cfg(feature = "impersonation")]
242 {
243 let sessions: Vec<Uuid> = ended.iter().map(|record| record.session).collect();
244 let ended = crate::impersonation::ending_in_sessions(read, &sessions, now).await?;
245 Ok(Impersonations { ended })
246 }
247 #[cfg(not(feature = "impersonation"))]
248 {
249 let _ = (read, ended, now);
250 Ok(Impersonations::default())
251 }
252}
253
254pub(crate) async fn ending_all(
255 read: &Read,
256 account: AccountId,
257 keep: Option<Uuid>,
258 now: Instant,
259) -> Result<Vec<AccountSession>> {
260 let mut live = AccountSession::query()
261 .where_(AccountSession::ACCOUNT.eq(account))
262 .where_(AccountSession::ENDED_AT.is_null())
263 .all(read)
264 .await?;
265 live.retain(|record| Some(record.session) != keep);
266 for record in &mut live {
267 record.end(now);
268 }
269 Ok(live)
270}
271
272pub(crate) async fn ending_everywhere(
273 read: &Read,
274 account: AccountId,
275 now: Instant,
276) -> Result<(Vec<AccountSession>, Impersonations)> {
277 let sessions = ending_all(read, account, None, now).await?;
278 let impersonations = impersonations_in(read, &sessions, now).await?;
279 Ok((sessions, impersonations))
280}
281
282pub async fn ending_in(
285 read: &Read,
286 account: AccountId,
287 area: &str,
288 now: Instant,
289) -> Result<Vec<AccountSession>> {
290 let mut live = AccountSession::query()
291 .where_(AccountSession::ACCOUNT.eq(account))
292 .where_(AccountSession::AREA.eq(area.to_owned()))
293 .where_(AccountSession::ENDED_AT.is_null())
294 .all(read)
295 .await?;
296 for record in &mut live {
297 record.end(now);
298 }
299 Ok(live)
300}
301
302pub async fn ending_as(
306 read: &Read,
307 accounts: &[AccountId],
308 area: &str,
309 principal: Uuid,
310 now: Instant,
311) -> Result<Vec<AccountSession>> {
312 let mut live = AccountSession::query()
313 .where_(AccountSession::ACCOUNT.in_(accounts.iter().copied()))
314 .where_(AccountSession::AREA.eq(area.to_owned()))
315 .where_(AccountSession::ENDED_AT.is_null())
316 .all(read)
317 .await?;
318 live.retain(|record| {
319 record
320 .principal
321 .is_none_or(|signed_in| signed_in == principal)
322 });
323 for record in &mut live {
324 record.end(now);
325 }
326 Ok(live)
327}
328
329#[cfg(feature = "account-data")]
330pub(crate) async fn exported(read: &Read, account: AccountId) -> Result<sofie::serde_json::Value> {
331 let sessions = AccountSession::query()
332 .where_(AccountSession::ACCOUNT.eq(account))
333 .all(read)
334 .await?;
335 Ok(sofie::serde_json::json!(
336 sessions
337 .iter()
338 .map(|session| sofie::serde_json::json!({
339 "area": session.area,
340 "device": session.device,
341 "last_seen_at": session.last_seen_at,
342 "ended_at": session.ended_at,
343 }))
344 .collect::<Vec<_>>()
345 ))
346}
347
348#[cfg(feature = "account-data")]
350pub(crate) async fn closing(
351 read: &Read,
352 account: AccountId,
353 now: Instant,
354) -> Result<Vec<AccountSession>> {
355 let mut sessions = AccountSession::query()
356 .where_(AccountSession::ACCOUNT.eq(account))
357 .all(read)
358 .await?;
359 for session in &mut sessions {
360 session.end(now);
361 session.device = None;
362 }
363 Ok(sessions)
364}