sofie_identity/tokens/
mod.rs1mod factory;
2#[cfg(feature = "token-screens")]
3pub mod screen;
4
5use std::time::Duration;
6
7use sofie::core::Instant;
8use sofie::db::{Db, in_select};
9use sofie::http::Bearer;
10use sofie::prelude::*;
11
12pub use factory::{Issued, issue, list, revoke};
13
14use crate::AccountId;
15use crate::membership::Membership;
16
17const LAST_USE_PRECISION: Duration = Duration::from_secs(60);
19
20#[derive(Aggregate)]
22#[aggregate(behaviors(SoftDelete))]
23pub struct AccessToken {
24 meta: Meta,
25 #[owner]
26 account: AccountId,
27 area: String,
28 digest: String,
29 scopes: String,
30 expires_at: Instant,
31 last_used_at: Option<Instant>,
32}
33
34pub type AccessTokenId = Id<AccessToken>;
35
36impl AccessToken {
37 #[must_use]
38 pub fn account(&self) -> AccountId {
39 self.account
40 }
41
42 #[must_use]
43 pub fn area(&self) -> &str {
44 &self.area
45 }
46
47 pub fn scopes(&self) -> impl Iterator<Item = &str> {
48 self.scopes.split_whitespace()
49 }
50
51 #[must_use]
52 pub fn expires_at(&self) -> Instant {
53 self.expires_at
54 }
55
56 #[must_use]
57 pub fn last_used_at(&self) -> Option<Instant> {
58 self.last_used_at
59 }
60}
61
62pub(crate) async fn bearer(db: &Db, area: &str, digest: &str) -> Result<Option<Bearer>> {
64 let members = Membership::AREA.eq(area.to_owned());
65 let found = AccessToken::query()
66 .where_(AccessToken::DIGEST.eq(digest.to_owned()))
67 .where_(AccessToken::AREA.eq(area.to_owned()))
68 .where_(in_select(
69 AccessToken::ACCOUNT,
70 Membership::ACCOUNT,
71 members,
72 ))
73 .first(&db.read())
74 .await?;
75 let now = db.clock().now();
76 let Some(mut token) = found.filter(|token| token.expires_at > now) else {
77 return Ok(None);
78 };
79 record_use(db, &mut token, now).await?;
80 Ok(Some(Bearer {
81 token: token.id().uuid(),
82 principal: token.account.uuid(),
83 scopes: token.scopes().map(str::to_owned).collect(),
84 }))
85}
86
87async fn record_use(db: &Db, token: &mut AccessToken, now: Instant) -> Result<()> {
88 let recent_since = now.checked_sub(LAST_USE_PRECISION)?;
89 if token.last_used_at.is_some_and(|used| used > recent_since) {
90 return Ok(());
91 }
92 token.last_used_at = Some(now);
93 match db.commit(token).await {
94 Err(Error::Conflict) => Ok(()),
96 recorded => recorded,
97 }
98}
99
100#[cfg(feature = "account-data")]
102pub(crate) async fn exported(read: &Read, account: AccountId) -> Result<sofie::serde_json::Value> {
103 let tokens = list(read, account).await?;
104 Ok(sofie::serde_json::json!(
105 tokens
106 .iter()
107 .map(|token| sofie::serde_json::json!({
108 "area": token.area,
109 "scopes": token.scopes().collect::<Vec<_>>(),
110 "expires_at": token.expires_at,
111 "last_used_at": token.last_used_at,
112 }))
113 .collect::<Vec<_>>()
114 ))
115}
116
117pub(crate) async fn revoking(read: &Read, account: AccountId) -> Result<Vec<AccessToken>> {
118 let mut tokens = list(read, account).await?;
119 for token in &mut tokens {
120 token.delete();
121 }
122 Ok(tokens)
123}