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