Skip to main content

shared/models/node/
enrollment.rs

1use 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    /// Mints a single-use code that lets Wings fetch its identity from the panel. The
28    /// code is bound to the node's current token id; redeeming rotates the token, so a
29    /// second redemption (or any older join command) stops working.
30    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}