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