1use std::{sync::Arc, time::Duration};
5
6use jiff::Timestamp;
7use topcoat::{
8 context::Cx,
9 session::{self, TokenHash},
10};
11use uuid::Uuid;
12
13use super::{
14 DynAuthenticator, PanelUser, SignedIn, infrastructure_failure,
15 token::{CREDENTIALS, REVOCATION, TokenPurpose},
16};
17use crate::auth::realm::{Realm, current_realm, realms};
18
19pub const SESSION_LIFETIME: Duration = Duration::from_hours(24 * 7);
21
22#[derive(Debug, Clone, toasty::Model)]
40pub struct AuthSession {
41 #[key]
43 pub token_hash: String,
44 #[index]
46 pub user_id: String,
47 pub panel: String,
50 pub tenant: Option<Uuid>,
52 #[index]
54 pub expires_at: Timestamp,
55 pub created_at: Timestamp,
56}
57
58pub(super) fn token_key(hash: &TokenHash) -> String {
59 use std::fmt::Write as _;
60
61 let mut key = String::with_capacity(64);
62 for byte in hash.iter() {
63 write!(key, "{byte:02x}").expect("writing to a String cannot fail");
64 }
65 key
66}
67
68pub(super) async fn record(
69 cx: &Cx,
70 session: &session::Session,
71 user: &dyn PanelUser,
72 panel: &Realm,
73) -> topcoat::Result<()> {
74 let expires_at = Timestamp::try_from(session.expires_at).map_err(topcoat::Error::from)?;
75 insert(
76 &mut crate::db::db(cx)?,
77 &session.token_hash,
78 user.user_id(),
79 panel.prefix.clone(),
80 None,
81 expires_at,
82 )
83 .await
84 .map_err(infrastructure_failure)
85}
86
87pub(super) fn credential_panels(
91 cx: &Cx,
92 user_type: std::any::TypeId,
93 own: &Arc<Realm>,
94) -> Vec<Arc<Realm>> {
95 let realms = realms(cx);
96 if realms.is_empty() {
97 return vec![Arc::clone(own)];
98 }
99 realms
100 .into_iter()
101 .filter(|realm| {
102 realm
103 .auth
104 .authenticator()
105 .is_some_and(|auth| auth.user_type() == user_type)
106 })
107 .collect()
108}
109
110pub(super) async fn end_credentials(
114 ex: &mut dyn toasty::Executor,
115 user_id: &str,
116 panels: &[Arc<Realm>],
117 purposes: &[TokenPurpose],
118) -> topcoat::Result<()> {
119 let prefixes: Vec<String> = panels.iter().map(|panel| panel.prefix.clone()).collect();
120 AuthSession::filter(
121 AuthSession::fields()
122 .user_id()
123 .eq(user_id.to_string())
124 .and(AuthSession::fields().panel().in_list(prefixes)),
125 )
126 .delete()
127 .exec(&mut *ex)
128 .await
129 .map_err(infrastructure_failure)?;
130 let issuing: Vec<String> = panels
131 .iter()
132 .filter(|panel| panel.auth.issues_tokens())
133 .map(|panel| panel.prefix.clone())
134 .collect();
135 if issuing.is_empty() {
136 return Ok(());
138 }
139 super::token::end_all(ex, user_id, Some(issuing), purposes).await
140}
141
142pub(super) async fn replace_sessions(
147 cx: &Cx,
148 ex: &mut dyn toasty::Executor,
149 signed: &SignedIn,
150 user_type: std::any::TypeId,
151 session: &session::Session,
152) -> topcoat::Result<()> {
153 let panels = credential_panels(cx, user_type, &signed.realm);
154 let user_id = signed.user.user_id();
155 end_credentials(&mut *ex, &user_id, &panels, CREDENTIALS).await?;
156 let expires_at = Timestamp::try_from(session.expires_at).map_err(topcoat::Error::from)?;
157 insert(
158 ex,
159 &session.token_hash,
160 user_id,
161 signed.realm.prefix.clone(),
162 signed.tenant,
163 expires_at,
164 )
165 .await
166 .map_err(infrastructure_failure)
167}
168
169#[doc(hidden)]
175pub async fn mint_session(
176 ex: &mut dyn toasty::Executor,
177 panel: &str,
178 user: &dyn PanelUser,
179) -> topcoat::Result<String> {
180 let token = session::Token::random();
181 let expires_at = Timestamp::try_from(std::time::SystemTime::now() + SESSION_LIFETIME)
182 .map_err(topcoat::Error::from)?;
183 insert(
184 ex,
185 &token.hash(),
186 user.user_id(),
187 crate::naming::normalize_prefix(panel),
188 None,
189 expires_at,
190 )
191 .await?;
192 Ok(token.encode())
193}
194
195async fn insert(
196 ex: &mut dyn toasty::Executor,
197 hash: &TokenHash,
198 user_id: String,
199 panel: String,
200 tenant: Option<Uuid>,
201 expires_at: Timestamp,
202) -> toasty::Result<()> {
203 toasty::create!(AuthSession {
204 token_hash: token_key(hash),
205 user_id,
206 panel,
207 tenant,
208 expires_at,
209 created_at: Timestamp::now(),
210 })
211 .exec(ex)
212 .await?;
213 Ok(())
214}
215
216pub(super) async fn select_tenant(cx: &Cx, tenant: Uuid) -> topcoat::Result<()> {
217 let Some(hash) = session::token_hash(cx).await? else {
218 return Err(topcoat::router::error::forbidden().into());
219 };
220 let mut db = crate::db::db(cx)?;
221 AuthSession::filter(AuthSession::fields().token_hash().eq(token_key(&hash)))
222 .update()
223 .tenant(Some(tenant))
224 .exec(&mut db)
225 .await
226 .map_err(infrastructure_failure)?;
227 Ok(())
228}
229
230pub(super) async fn delete_session(cx: &Cx, hash: &TokenHash) -> topcoat::Result<()> {
231 delete_session_row(cx, &token_key(hash)).await
232}
233
234pub(super) async fn start_once(
238 cx: &Cx,
239 started: &mut Option<session::Session>,
240) -> topcoat::Result<session::Session> {
241 if let Some(session) = started {
242 return Ok(session.clone());
243 }
244 let session = session::start(cx).await?;
245 *started = Some(session.clone());
246 Ok(session)
247}
248
249pub(super) async fn fresh_session(cx: &Cx) -> topcoat::Result<session::Session> {
256 if let Some(hash) = session::token_hash(cx)
257 .await
258 .map_err(infrastructure_failure)?
259 {
260 delete_session(cx, &hash).await?;
261 }
262 session::start(cx).await.map_err(infrastructure_failure)
263}
264
265async fn delete_session_row(cx: &Cx, key: &str) -> topcoat::Result<()> {
266 let mut db = crate::db::db(cx)?;
267 AuthSession::filter(AuthSession::fields().token_hash().eq(key.to_string()))
268 .delete()
269 .exec(&mut db)
270 .await
271 .map_err(infrastructure_failure)?;
272 Ok(())
273}
274
275pub async fn revoke_sessions_for_user(cx: &Cx, user_id: &str) -> topcoat::Result<()> {
284 let mut db = crate::db::db(cx)?;
285 if let Some(panel) = current_realm(cx) {
286 let panels = match panel.auth.authenticator() {
287 Some(auth) => credential_panels(cx, auth.user_type(), panel),
288 None => vec![Arc::clone(panel)],
289 };
290 end_credentials(&mut db, user_id, &panels, REVOCATION).await?;
291 wake_bells(
292 cx,
293 user_id,
294 panels.iter().map(|panel| panel.prefix.as_str()),
295 );
296 return Ok(());
297 }
298 AuthSession::filter(AuthSession::fields().user_id().eq(user_id.to_string()))
299 .delete()
300 .exec(&mut db)
301 .await
302 .map_err(infrastructure_failure)?;
303 let realms = realms(cx);
304 if realms.iter().any(|realm| realm.auth.issues_tokens()) {
305 super::token::end_all(&mut db, user_id, None, REVOCATION).await?;
306 }
307 wake_bells(
308 cx,
309 user_id,
310 realms.iter().map(|realm| realm.prefix.as_str()),
311 );
312 Ok(())
313}
314
315fn wake_bells<'a>(cx: &Cx, user_id: &str, panels: impl Iterator<Item = &'a str>) {
317 let bus = crate::bus::bus(cx);
318 for panel in panels {
319 bus.wake(panel, user_id);
320 }
321}
322
323pub(super) const SESSION_SWEEP_BATCH: usize = 500;
326
327pub(super) async fn sweep_expired_sessions(cx: &Cx) -> topcoat::Result<()> {
329 let now = Timestamp::now();
330 let mut db = crate::db::db(cx)?;
331 let expired: Vec<String> = AuthSession::filter(AuthSession::fields().expires_at().le(now))
332 .limit(SESSION_SWEEP_BATCH)
333 .exec(&mut db)
334 .await
335 .map_err(infrastructure_failure)?
336 .into_iter()
337 .map(|row| row.token_hash)
338 .collect();
339 if expired.is_empty() {
340 return Ok(());
341 }
342 AuthSession::filter(
344 AuthSession::fields()
345 .token_hash()
346 .in_list(expired)
347 .and(AuthSession::fields().expires_at().le(now)),
348 )
349 .delete()
350 .exec(&mut db)
351 .await
352 .map_err(infrastructure_failure)?;
353 Ok(())
354}
355
356pub(super) async fn session_row(cx: &Cx) -> topcoat::Result<Option<AuthSession>> {
359 let Some(hash) = session::token_hash(cx).await? else {
360 return Ok(None);
361 };
362 let key = token_key(&hash);
363 let mut db = crate::db::db(cx)?;
364 let row = AuthSession::filter(AuthSession::fields().token_hash().eq(key))
365 .first()
366 .exec(&mut db)
367 .await
368 .map_err(infrastructure_failure)?;
369 let Some(row) = row else {
370 return Ok(None);
371 };
372 if row.expires_at <= Timestamp::now() {
373 delete_session_row(cx, &row.token_hash).await?;
374 return Ok(None);
375 }
376 Ok(Some(row))
377}
378
379pub(super) async fn session_user(
382 cx: &Cx,
383 row: AuthSession,
384 panel: &Arc<Realm>,
385 authenticator: &dyn DynAuthenticator,
386) -> topcoat::Result<Option<SignedIn>> {
387 match authenticator
388 .find_by_id(cx, &row.user_id)
389 .await
390 .map_err(infrastructure_failure)?
391 {
392 Some(user) => Ok(Some(SignedIn {
393 user,
394 realm: Arc::clone(panel),
395 tenant: row.tenant,
396 })),
397 None => {
398 delete_session_row(cx, &row.token_hash).await?;
399 Ok(None)
400 }
401 }
402}
403
404pub(super) async fn resolve(
407 cx: &Cx,
408 panel: &Arc<Realm>,
409 authenticator: &dyn DynAuthenticator,
410) -> topcoat::Result<Option<SignedIn>> {
411 match session_row(cx).await? {
412 Some(row) if row.panel == panel.prefix => session_user(cx, row, panel, authenticator).await,
413 _ => Ok(None),
414 }
415}