1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9 collections::BTreeMap,
10 sync::{Arc, LazyLock},
11};
12use tundra_common::hash::Hash32;
13use utoipa::ToSchema;
14
15#[derive(Serialize, Deserialize)]
16pub struct NodeTunnel {
17 pub node: Fetchable<super::node::Node>,
18
19 pub host: compact_str::CompactString,
20 pub port: u16,
21
22 pub cert_sha256: Option<Hash32>,
23
24 pub created: chrono::NaiveDateTime,
25
26 extension_data: super::ModelExtensionData,
27}
28
29impl BaseModel for NodeTunnel {
30 const NAME: &'static str = "node_tunnel";
31
32 fn get_extension_list() -> &'static super::ModelExtensionList {
33 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
34 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
35
36 &EXTENSIONS
37 }
38
39 fn get_extension_data(&self) -> &super::ModelExtensionData {
40 &self.extension_data
41 }
42
43 #[inline]
44 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
45 let prefix = prefix.unwrap_or_default();
46
47 BTreeMap::from([
48 (
49 "node_tunnels.node_uuid",
50 compact_str::format_compact!("{prefix}node_uuid"),
51 ),
52 (
53 "node_tunnels.host",
54 compact_str::format_compact!("{prefix}host"),
55 ),
56 (
57 "node_tunnels.port",
58 compact_str::format_compact!("{prefix}port"),
59 ),
60 (
61 "node_tunnels.cert_sha256",
62 compact_str::format_compact!("{prefix}cert_sha256"),
63 ),
64 (
65 "node_tunnels.created",
66 compact_str::format_compact!("{prefix}created"),
67 ),
68 ])
69 }
70
71 #[inline]
72 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
73 let prefix = prefix.unwrap_or_default();
74
75 Ok(Self {
76 node: super::node::Node::get_fetchable(
77 row.try_get(compact_str::format_compact!("{prefix}node_uuid").as_str())?,
78 ),
79 host: row.try_get(compact_str::format_compact!("{prefix}host").as_str())?,
80 port: row.try_get::<i32, _>(compact_str::format_compact!("{prefix}port").as_str())?
81 as u16,
82 cert_sha256: row
83 .try_get::<Option<Vec<u8>>, _>(
84 compact_str::format_compact!("{prefix}cert_sha256").as_str(),
85 )?
86 .and_then(|bytes| <[u8; 32]>::try_from(bytes.as_slice()).ok())
87 .map(Hash32),
88 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
89 extension_data: Self::map_extensions(prefix, row)?,
90 })
91 }
92}
93
94impl NodeTunnel {
95 pub async fn by_node_uuid(
96 database: &crate::database::Database,
97 node_uuid: uuid::Uuid,
98 ) -> Result<Option<Self>, crate::database::DatabaseError> {
99 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
100 r#"
101 SELECT {}
102 FROM node_tunnels
103 WHERE node_tunnels.node_uuid = $1
104 "#,
105 Self::columns_sql(None)
106 )))
107 .bind(node_uuid)
108 .fetch_optional(database.read())
109 .await?;
110
111 row.try_map(|row| Self::map(None, &row))
112 }
113
114 pub async fn store_cert(
115 database: &crate::database::Database,
116 node_uuid: uuid::Uuid,
117 cert_sha256: Option<Hash32>,
118 ) -> Result<bool, crate::database::DatabaseError> {
119 let mut transaction = database.write().begin().await?;
120
121 let affected = sqlx::query(
122 r#"
123 UPDATE node_tunnels
124 SET cert_sha256 = $2
125 WHERE node_tunnels.node_uuid = $1
126 "#,
127 )
128 .bind(node_uuid)
129 .bind(cert_sha256.map(|hash| hash.0.to_vec()))
130 .execute(&mut *transaction)
131 .await?
132 .rows_affected();
133
134 if affected == 0 {
135 return Ok(false);
136 }
137
138 crate::tunnel::bump_epoch(&mut *transaction).await?;
139 transaction.commit().await?;
140
141 Ok(true)
142 }
143}
144
145#[async_trait::async_trait]
146impl IntoAdminApiObject for NodeTunnel {
147 type AdminApiObject = AdminApiNodeTunnel;
148 type ExtraArgs<'a> = ();
149
150 async fn into_admin_api_object<'a>(
151 self,
152 state: &crate::State,
153 _args: Self::ExtraArgs<'a>,
154 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
155 let api_object = AdminApiNodeTunnel::init_hooks(&self, state).await?;
156
157 let api_object = finish_extendible!(
158 AdminApiNodeTunnel {
159 host: self.host,
160 port: self.port,
161 cert_sha256: self.cert_sha256.map(|hash| hash.to_hex().into()),
162 created: self.created.and_utc(),
163 },
164 api_object,
165 state
166 )?;
167
168 Ok(api_object)
169 }
170}
171
172#[schema_extension_derive::extendible]
173#[init_args(NodeTunnel, crate::State)]
174#[hook_args(crate::State)]
175#[derive(ToSchema, Serialize)]
176#[schema(title = "AdminNodeTunnel")]
177pub struct AdminApiNodeTunnel {
178 pub host: compact_str::CompactString,
179 pub port: u16,
180
181 pub cert_sha256: Option<compact_str::CompactString>,
182
183 pub created: chrono::DateTime<chrono::Utc>,
184}
185
186#[derive(ToSchema, Deserialize, Validate)]
187pub struct CreateNodeTunnelOptions {
188 #[garde(skip)]
189 pub node_uuid: uuid::Uuid,
190
191 #[garde(length(chars, min = 1, max = 255))]
192 #[schema(min_length = 1, max_length = 255)]
193 pub host: compact_str::CompactString,
194 #[garde(range(min = 1))]
195 #[schema(minimum = 1)]
196 pub port: u16,
197}
198
199#[async_trait::async_trait]
200impl CreatableModel for NodeTunnel {
201 type CreateOptions<'a> = CreateNodeTunnelOptions;
202 type CreateResult = Self;
203
204 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
205 static CREATE_LISTENERS: LazyLock<CreateListenerList<NodeTunnel>> =
206 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
207
208 &CREATE_LISTENERS
209 }
210
211 async fn create_with_transaction(
212 state: &crate::State,
213 mut options: Self::CreateOptions<'_>,
214 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
215 ) -> Result<Self, crate::database::DatabaseError> {
216 options.validate()?;
217
218 let mut query_builder = InsertQueryBuilder::new("node_tunnels");
219
220 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
221
222 query_builder
223 .set("node_uuid", options.node_uuid)
224 .set("host", &options.host)
225 .set("port", options.port as i32);
226
227 let row = query_builder
228 .returning(&Self::columns_sql(None))
229 .fetch_one(&mut **transaction)
230 .await?;
231 let mut node_tunnel = Self::map(None, &row)?;
232
233 crate::tunnel::bump_epoch(&mut **transaction).await?;
234
235 Self::run_after_create_handlers(&mut node_tunnel, &options, state, transaction).await?;
236
237 Ok(node_tunnel)
238 }
239}
240
241#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
242pub struct UpdateNodeTunnelOptions {
243 #[garde(length(chars, min = 1, max = 255))]
244 #[schema(min_length = 1, max_length = 255)]
245 pub host: Option<compact_str::CompactString>,
246 #[garde(range(min = 1))]
247 #[schema(minimum = 1)]
248 pub port: Option<u16>,
249}
250
251#[async_trait::async_trait]
252impl UpdatableModel for NodeTunnel {
253 type UpdateOptions = UpdateNodeTunnelOptions;
254
255 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
256 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<NodeTunnel>> =
257 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
258
259 &UPDATE_LISTENERS
260 }
261
262 async fn update_with_transaction(
263 &mut self,
264 state: &crate::State,
265 mut options: Self::UpdateOptions,
266 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
267 ) -> Result<(), crate::database::DatabaseError> {
268 options.validate()?;
269
270 let mut query_builder = UpdateQueryBuilder::new("node_tunnels");
271
272 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
273 .await?;
274
275 query_builder
276 .set("host", options.host.as_ref())
277 .set("port", options.port.map(|port| port as i32))
278 .where_eq("node_uuid", self.node.uuid);
279
280 query_builder.execute(&mut **transaction).await?;
281
282 crate::tunnel::bump_epoch(&mut **transaction).await?;
283
284 if let Some(host) = options.host {
285 self.host = host;
286 }
287 if let Some(port) = options.port {
288 self.port = port;
289 }
290
291 self.run_after_update_handlers(state, transaction).await?;
292
293 Ok(())
294 }
295}
296
297#[async_trait::async_trait]
298impl DeletableModel for NodeTunnel {
299 type DeleteOptions = ();
300
301 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
302 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<NodeTunnel>> =
303 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
304
305 &DELETE_LISTENERS
306 }
307
308 async fn delete_with_transaction(
309 &self,
310 state: &crate::State,
311 options: Self::DeleteOptions,
312 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
313 ) -> Result<(), anyhow::Error> {
314 self.run_delete_handlers(&options, state, transaction)
315 .await?;
316
317 sqlx::query(
318 r#"
319 DELETE FROM node_tunnels
320 WHERE node_tunnels.node_uuid = $1
321 "#,
322 )
323 .bind(self.node.uuid)
324 .execute(&mut **transaction)
325 .await?;
326
327 crate::tunnel::bump_epoch(&mut **transaction).await?;
328
329 self.run_after_delete_handlers(&options, state, transaction)
330 .await?;
331
332 Ok(())
333 }
334}