Skip to main content

tuwunel_service/registration_tokens/
data.rs

1//! Persistent registration token metadata.
2//!
3//! Database tokens retain a stored successful-use count and optional expiration limits. Validation
4//! removes records when the stored count reaches its threshold or the deadline passes.
5
6use std::{sync::Arc, time::SystemTime};
7
8use futures::Stream;
9use serde::{Deserialize, Serialize};
10use tuwunel_core::{
11	Err, Result, err,
12	utils::{
13		self,
14		stream::{ReadyExt, TryIgnore},
15	},
16};
17use tuwunel_database::{Database, Deserialized, Json, Map};
18
19/// Provides database operations for registration tokens.
20///
21/// Each token maps directly to its serialized [`DatabaseTokenInfo`]. Expired or exhausted tokens
22/// are removed by validation and iteration paths.
23pub(super) struct Data {
24	registrationtoken_info: Arc<Map>,
25}
26
27/// Persistent usage and expiration metadata for a registration token.
28///
29/// Each successful consumption writes an incremented counter snapshot. Concurrent consumers are
30/// not serialized and can share a prior count. Expiration limits are evaluated when the token is
31/// checked or listed.
32#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
33pub struct DatabaseTokenInfo {
34	/// The stored number of successful token consumptions.
35	pub uses: u64,
36
37	/// When this token will expire, if it expires.
38	pub expires: TokenExpires,
39}
40
41impl DatabaseTokenInfo {
42	/// Creates unused metadata with the supplied expiration policy.
43	///
44	/// New tokens always begin with a zero use count.
45	pub(super) fn new(expires: TokenExpires) -> Self { Self { uses: 0, expires } }
46
47	/// Reports whether the token remains within every expiration limit.
48	///
49	/// A token is invalid after using its full allowance or passing its absolute deadline. Metadata
50	/// without either limit remains valid indefinitely.
51	#[must_use]
52	pub fn is_valid(&self) -> bool {
53		if let Some(max_uses) = self.expires.max_uses
54			&& self.uses >= max_uses
55		{
56			return false;
57		}
58
59		if let Some(max_age) = self.expires.max_age {
60			let now = SystemTime::now();
61
62			if now > max_age {
63				return false;
64			}
65		}
66
67		true
68	}
69}
70
71impl std::fmt::Display for DatabaseTokenInfo {
72	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73		write!(f, "Token used {} times. {}", self.uses, self.expires)?;
74
75		Ok(())
76	}
77}
78
79/// Optional limits governing a database-backed registration token.
80///
81/// Either limit can be absent independently. When both are absent, the token does not expire.
82#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
83pub struct TokenExpires {
84	/// Stored use-count threshold at which the token becomes invalid.
85	pub max_uses: Option<u64>,
86
87	/// Absolute time after which the token is invalid.
88	pub max_age: Option<SystemTime>,
89}
90
91impl std::fmt::Display for TokenExpires {
92	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93		let mut msgs = vec![];
94
95		if let Some(max_uses) = self.max_uses {
96			msgs.push(format!("after {max_uses} uses"));
97		}
98
99		if let Some(max_age) = self.max_age {
100			let now = SystemTime::now();
101			let expires_at = utils::time::format(max_age, "%F %T");
102
103			match max_age.duration_since(now) {
104				| Ok(duration) => {
105					let expires_in = utils::time::pretty(duration);
106					msgs.push(format!("in {expires_in} ({expires_at})"));
107				},
108				| Err(_) => {
109					write!(f, "Expired at {expires_at}")?;
110					return Ok(());
111				},
112			}
113		}
114
115		if !msgs.is_empty() {
116			write!(f, "Expires {}.", msgs.join(" or "))?;
117		} else {
118			write!(f, "Never expires.")?;
119		}
120
121		Ok(())
122	}
123}
124
125impl Data {
126	/// Opens the registration token metadata map.
127	///
128	/// The returned handle shares the caller's database instance.
129	pub(super) fn new(db: &Arc<Database>) -> Self {
130		Self {
131			registrationtoken_info: db["registrationtoken_info"].clone(),
132		}
133	}
134
135	/// Stores a new registration token and its expiration policy.
136	///
137	/// The token begins with zero uses. A token already present in the database is rejected.
138	pub(super) async fn save_token(
139		&self,
140		token: &str,
141		expires: TokenExpires,
142	) -> Result<DatabaseTokenInfo> {
143		if self
144			.registrationtoken_info
145			.exists(token)
146			.await
147			.is_err()
148		{
149			let info = DatabaseTokenInfo::new(expires);
150
151			self.registrationtoken_info
152				.raw_put(token, Json(&info));
153
154			Ok(info)
155		} else {
156			Err!(Request(InvalidParam("Registration token already exists")))
157		}
158	}
159
160	/// Deletes a registration token from persistent storage.
161	///
162	/// Unknown tokens produce a not-found request error.
163	pub(super) async fn revoke_token(&self, token: &str) -> Result {
164		if self
165			.registrationtoken_info
166			.exists(token)
167			.await
168			.is_ok()
169		{
170			self.registrationtoken_info.remove(token);
171
172			Ok(())
173		} else {
174			Err!(Request(NotFound("Registration token not found")))
175		}
176	}
177
178	/// Checks a stored token and optionally consumes one use.
179	///
180	/// Invalid tokens are deleted. A successful consumption writes an incremented count or removes the
181	/// token when the updated count reaches its threshold. Concurrent checks are not serialized.
182	pub(super) async fn check_token(&self, token: &str, consume: bool) -> bool {
183		let info = self
184			.registrationtoken_info
185			.get(token)
186			.await
187			.deserialized::<DatabaseTokenInfo>()
188			.ok();
189
190		info.map(|mut info| {
191			if !info.is_valid() {
192				self.registrationtoken_info.remove(token);
193				return false;
194			}
195
196			if consume {
197				info.uses = info.uses.saturating_add(1);
198
199				if info.is_valid() {
200					self.registrationtoken_info
201						.raw_put(token, Json(info));
202				} else {
203					self.registrationtoken_info.remove(token);
204				}
205			}
206
207			true
208		})
209		.unwrap_or(false)
210	}
211
212	/// Loads a database token's stored metadata.
213	///
214	/// The metadata is returned without checking expiration. Missing or undecodable rows produce a
215	/// not-found request error.
216	pub(super) async fn get_token_info(&self, token: &str) -> Result<DatabaseTokenInfo> {
217		self.registrationtoken_info
218			.get(token)
219			.await
220			.deserialized()
221			.map_err(|_| err!(Request(NotFound("Registration token not found"))))
222	}
223
224	/// Replaces a token's expiration policy while preserving its use count.
225	///
226	/// The token must already exist. The updated metadata is persisted and returned.
227	pub(super) async fn update_token(
228		&self,
229		token: &str,
230		expires: TokenExpires,
231	) -> Result<DatabaseTokenInfo> {
232		let current = self.get_token_info(token).await?;
233
234		let info = DatabaseTokenInfo { uses: current.uses, expires };
235
236		self.registrationtoken_info
237			.raw_put(token, Json(&info));
238
239		Ok(info)
240	}
241
242	/// Streams every valid database token and removes invalid entries.
243	///
244	/// Unreadable rows are skipped before validation. Expired or exhausted tokens are deleted as the
245	/// stream is consumed.
246	pub(super) fn iterate_and_clean_tokens(
247		&self,
248	) -> impl Stream<Item = (&str, DatabaseTokenInfo)> + Send + '_ {
249		self.registrationtoken_info
250			.stream()
251			.ignore_err()
252			.ready_filter_map(|(token, info): (&str, DatabaseTokenInfo)| {
253				if info.is_valid() {
254					Some((token, info))
255				} else {
256					self.registrationtoken_info.remove(token);
257					None
258				}
259			})
260	}
261}