shared/models/node/
enrollment.rs1use super::Node;
2use crate::models::ByUuid;
3use rand::distr::SampleString;
4use serde::{Deserialize, Serialize};
5
6const ENROLLMENT_TTL_SECONDS: u64 = 30 * 60;
7
8#[derive(Serialize, Deserialize)]
9struct StoredEnrollment {
10 node_uuid: uuid::Uuid,
11 token_id: compact_str::CompactString,
12 remote: Option<compact_str::CompactString>,
13}
14
15pub struct NodeEnrollment {
16 pub node: Node,
17 pub remote: Option<compact_str::CompactString>,
18 pub token_id: String,
19 pub token: String,
20}
21
22fn cache_key(code: &str) -> String {
23 format!("node_enrollment::{}", crate::crypt::token_digest(code))
24}
25
26impl NodeEnrollment {
27 pub async fn create(
31 state: &crate::State,
32 node: &Node,
33 remote: Option<&str>,
34 ) -> Result<(String, chrono::DateTime<chrono::Utc>), anyhow::Error> {
35 let code = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 32);
36
37 state
38 .cache
39 .set(
40 &cache_key(&code),
41 ENROLLMENT_TTL_SECONDS,
42 &StoredEnrollment {
43 node_uuid: node.uuid,
44 token_id: node.token_id.clone(),
45 remote: remote.map(Into::into),
46 },
47 )
48 .await?;
49
50 Ok((
51 code,
52 chrono::Utc::now() + chrono::Duration::seconds(ENROLLMENT_TTL_SECONDS as i64),
53 ))
54 }
55
56 pub async fn redeem(state: &crate::State, code: &str) -> Result<Option<Self>, anyhow::Error> {
57 let key = cache_key(code);
58 let Some(stored) = state.cache.get::<StoredEnrollment>(&key).await? else {
59 return Ok(None);
60 };
61 state.cache.invalidate(&key).await?;
62
63 let Some(node) = Node::by_uuid_optional(&state.database, stored.node_uuid).await? else {
64 return Ok(None);
65 };
66
67 let Some((token_id, token)) = node
68 .reset_token_if_unchanged(state, &stored.token_id)
69 .await?
70 else {
71 return Ok(None);
72 };
73
74 Ok(Some(Self {
75 node,
76 remote: stored.remote,
77 token_id,
78 token,
79 }))
80 }
81}