aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs5
-rw-r--r--src/db.rs11
-rw-r--r--src/error.rs3
-rw-r--r--src/main.rs98
-rw-r--r--src/routes.rs2
-rw-r--r--src/routes/device.rs91
-rw-r--r--src/routes/start.rs16
-rw-r--r--src/services/ping.rs31
8 files changed, 199 insertions, 58 deletions
diff --git a/src/config.rs b/src/config.rs
index 4319ffc..9605361 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -7,6 +7,8 @@ pub struct Config {
7 pub apikey: String, 7 pub apikey: String,
8 pub serveraddr: String, 8 pub serveraddr: String,
9 pub pingtimeout: i64, 9 pub pingtimeout: i64,
10 pub pingthreshold: i64,
11 pub timeoffset: i8,
10} 12}
11 13
12impl Config { 14impl Config {
@@ -14,6 +16,8 @@ impl Config {
14 let config = config::Config::builder() 16 let config = config::Config::builder()
15 .set_default("serveraddr", "0.0.0.0:7229")? 17 .set_default("serveraddr", "0.0.0.0:7229")?
16 .set_default("pingtimeout", 10)? 18 .set_default("pingtimeout", 10)?
19 .set_default("pingthreshold", 1)?
20 .set_default("timeoffset", 0)?
17 .add_source(File::with_name("config.toml").required(false)) 21 .add_source(File::with_name("config.toml").required(false))
18 .add_source(File::with_name("config.dev.toml").required(false)) 22 .add_source(File::with_name("config.dev.toml").required(false))
19 .add_source(config::Environment::with_prefix("WEBOL").prefix_separator("_")) 23 .add_source(config::Environment::with_prefix("WEBOL").prefix_separator("_"))
@@ -22,4 +26,3 @@ impl Config {
22 config.try_deserialize() 26 config.try_deserialize()
23 } 27 }
24} 28}
25
diff --git a/src/db.rs b/src/db.rs
index 47e907d..a2b2009 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -1,6 +1,7 @@
1use serde::Serialize; 1use serde::Serialize;
2use sqlx::{PgPool, postgres::PgPoolOptions, types::{ipnetwork::IpNetwork, mac_address::MacAddress}}; 2use sqlx::{PgPool, postgres::PgPoolOptions, types::{ipnetwork::IpNetwork, mac_address::MacAddress}};
3use tracing::{debug, info}; 3use tracing::{debug, info};
4use utoipa::ToSchema;
4 5
5#[derive(Serialize, Debug)] 6#[derive(Serialize, Debug)]
6pub struct Device { 7pub struct Device {
@@ -11,6 +12,16 @@ pub struct Device {
11 pub times: Option<Vec<i64>> 12 pub times: Option<Vec<i64>>
12} 13}
13 14
15#[derive(ToSchema)]
16#[schema(as = Device)]
17pub struct DeviceSchema {
18 pub id: String,
19 pub mac: String,
20 pub broadcast_addr: String,
21 pub ip: String,
22 pub times: Option<Vec<i64>>
23}
24
14pub async fn init_db_pool(db_url: &str) -> PgPool { 25pub async fn init_db_pool(db_url: &str) -> PgPool {
15 debug!("attempt to connect dbPool to '{}'", db_url); 26 debug!("attempt to connect dbPool to '{}'", db_url);
16 27
diff --git a/src/error.rs b/src/error.rs
index 513b51b..006fcdb 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -5,10 +5,11 @@ use axum::response::{IntoResponse, Response};
5use axum::Json; 5use axum::Json;
6use mac_address::MacParseError; 6use mac_address::MacParseError;
7use serde_json::json; 7use serde_json::json;
8use utoipa::ToSchema;
8use std::io; 9use std::io;
9use tracing::error; 10use tracing::error;
10 11
11#[derive(Debug, thiserror::Error)] 12#[derive(Debug, thiserror::Error, ToSchema)]
12pub enum Error { 13pub enum Error {
13 #[error("db: {source}")] 14 #[error("db: {source}")]
14 Db { 15 Db {
diff --git a/src/main.rs b/src/main.rs
index d17984f..00fc6ce 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,21 +1,30 @@
1use crate::config::Config; 1use crate::{
2use crate::db::init_db_pool; 2 config::Config,
3use crate::routes::device; 3 db::init_db_pool,
4use crate::routes::start::start; 4 routes::{device, start, status},
5use crate::routes::status::status; 5 services::ping::{BroadcastCommand, StatusMap},
6use crate::services::ping::StatusMap; 6};
7use axum::middleware::from_fn_with_state; 7use axum::{
8use axum::routing::{get, put}; 8 middleware::from_fn_with_state,
9use axum::{routing::post, Router}; 9 routing::{get, post},
10 Router,
11};
10use dashmap::DashMap; 12use dashmap::DashMap;
11use services::ping::BroadcastCommand;
12use sqlx::PgPool; 13use sqlx::PgPool;
13use std::env; 14use time::UtcOffset;
14use std::sync::Arc; 15use std::{env, sync::Arc};
15use tokio::sync::broadcast::{channel, Sender}; 16use tokio::sync::broadcast::{channel, Sender};
16use tracing::{info, level_filters::LevelFilter}; 17use tracing::{info, level_filters::LevelFilter};
17use tracing_subscriber::fmt::time::UtcTime; 18use tracing_subscriber::{
18use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 19 fmt::{self, time::OffsetTime},
20 prelude::*,
21 EnvFilter,
22};
23use utoipa::{
24 openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
25 Modify, OpenApi,
26};
27use utoipa_swagger_ui::SwaggerUi;
19 28
20mod config; 29mod config;
21mod db; 30mod db;
@@ -25,20 +34,62 @@ mod routes;
25mod services; 34mod services;
26mod wol; 35mod wol;
27 36
37#[derive(OpenApi)]
38#[openapi(
39 paths(
40 start::start,
41 device::get,
42 device::get_path,
43 device::post,
44 device::put,
45 ),
46 components(
47 schemas(
48 start::Payload,
49 start::Response,
50 device::PutDevicePayload,
51 device::GetDevicePayload,
52 device::PostDevicePayload,
53 db::DeviceSchema,
54 )
55 ),
56 modifiers(&SecurityAddon),
57 tags(
58 (name = "Webol", description = "Webol API")
59 )
60)]
61struct ApiDoc;
62
63struct SecurityAddon;
64
65impl Modify for SecurityAddon {
66 fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
67 if let Some(components) = openapi.components.as_mut() {
68 components.add_security_scheme(
69 "api_key",
70 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("Authorization"))),
71 );
72 }
73 }
74}
75
28#[tokio::main] 76#[tokio::main]
77#[allow(deprecated)]
29async fn main() -> color_eyre::eyre::Result<()> { 78async fn main() -> color_eyre::eyre::Result<()> {
30 color_eyre::install()?; 79 color_eyre::install()?;
31 80
81 let config = Config::load()?;
82
32 let time_format = 83 let time_format =
33 time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); 84 time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
34 let loc = UtcTime::new(time_format); 85 let time = OffsetTime::new(UtcOffset::from_hms(config.timeoffset, 0, 0)?, time_format);
35 86
36 let file_appender = tracing_appender::rolling::daily("logs", "webol.log"); 87 let file_appender = tracing_appender::rolling::daily("logs", "webol.log");
37 let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); 88 let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
38 89
39 tracing_subscriber::registry() 90 tracing_subscriber::registry()
40 .with(fmt::layer().with_writer(non_blocking).with_ansi(false)) 91 .with(fmt::layer().with_writer(non_blocking).with_ansi(false))
41 .with(fmt::layer().with_timer(loc)) 92 .with(fmt::layer().with_timer(time))
42 .with( 93 .with(
43 EnvFilter::builder() 94 EnvFilter::builder()
44 .with_default_directive(LevelFilter::INFO.into()) 95 .with_default_directive(LevelFilter::INFO.into())
@@ -48,8 +99,6 @@ async fn main() -> color_eyre::eyre::Result<()> {
48 99
49 let version = env!("CARGO_PKG_VERSION"); 100 let version = env!("CARGO_PKG_VERSION");
50 101
51 let config = Config::load()?;
52
53 info!("start webol v{}", version); 102 info!("start webol v{}", version);
54 103
55 let db = init_db_pool(&config.database_url).await; 104 let db = init_db_pool(&config.database_url).await;
@@ -67,12 +116,15 @@ async fn main() -> color_eyre::eyre::Result<()> {
67 }; 116 };
68 117
69 let app = Router::new() 118 let app = Router::new()
70 .route("/start", post(start)) 119 .route("/start", post(start::start))
71 .route("/device", get(device::get)) 120 .route(
72 .route("/device", put(device::put)) 121 "/device",
73 .route("/device", post(device::post)) 122 post(device::post).get(device::get).put(device::put),
74 .route("/status", get(status)) 123 )
124 .route("/device/:id", get(device::get_path))
125 .route("/status", get(status::status))
75 .route_layer(from_fn_with_state(shared_state.clone(), extractors::auth)) 126 .route_layer(from_fn_with_state(shared_state.clone(), extractors::auth))
127 .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
76 .with_state(Arc::new(shared_state)); 128 .with_state(Arc::new(shared_state));
77 129
78 let addr = config.serveraddr; 130 let addr = config.serveraddr;
diff --git a/src/routes.rs b/src/routes.rs
index d5ab0d6..a72f27b 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -1,3 +1,3 @@
1pub mod start; 1pub mod start;
2pub mod device; 2pub mod device;
3pub mod status; \ No newline at end of file 3pub mod status;
diff --git a/src/routes/device.rs b/src/routes/device.rs
index d39d98e..d01d9f0 100644
--- a/src/routes/device.rs
+++ b/src/routes/device.rs
@@ -1,14 +1,25 @@
1use crate::db::Device; 1use crate::db::Device;
2use crate::error::Error; 2use crate::error::Error;
3use axum::extract::State; 3use axum::extract::{Path, State};
4use axum::Json; 4use axum::Json;
5use mac_address::MacAddress; 5use mac_address::MacAddress;
6use serde::{Deserialize, Serialize}; 6use serde::Deserialize;
7use serde_json::{json, Value}; 7use serde_json::{json, Value};
8use sqlx::types::ipnetwork::IpNetwork; 8use sqlx::types::ipnetwork::IpNetwork;
9use std::{sync::Arc, str::FromStr}; 9use std::{str::FromStr, sync::Arc};
10use tracing::{debug, info}; 10use tracing::{debug, info};
11use utoipa::ToSchema;
11 12
13#[utoipa::path(
14 get,
15 path = "/device",
16 request_body = GetDevicePayload,
17 responses(
18 (status = 200, description = "Get `Device` information", body = [Device])
19 ),
20 security(("api_key" = []))
21)]
22#[deprecated]
12pub async fn get( 23pub async fn get(
13 State(state): State<Arc<crate::AppState>>, 24 State(state): State<Arc<crate::AppState>>,
14 Json(payload): Json<GetDevicePayload>, 25 Json(payload): Json<GetDevicePayload>,
@@ -31,11 +42,53 @@ pub async fn get(
31 Ok(Json(json!(device))) 42 Ok(Json(json!(device)))
32} 43}
33 44
34#[derive(Deserialize)] 45#[utoipa::path(
46 get,
47 path = "/device/{id}",
48 responses(
49 (status = 200, description = "Get `Device` information", body = [Device])
50 ),
51 params(
52 ("id" = String, Path, description = "Device id")
53 ),
54 security(("api_key" = []))
55)]
56pub async fn get_path(
57 State(state): State<Arc<crate::AppState>>,
58 Path(path): Path<String>,
59) -> Result<Json<Value>, Error> {
60 info!("get device from path {}", path);
61 let device = sqlx::query_as!(
62 Device,
63 r#"
64 SELECT id, mac, broadcast_addr, ip, times
65 FROM devices
66 WHERE id = $1;
67 "#,
68 path
69 )
70 .fetch_one(&state.db)
71 .await?;
72
73 debug!("got device {:?}", device);
74
75 Ok(Json(json!(device)))
76}
77
78#[derive(Deserialize, ToSchema)]
35pub struct GetDevicePayload { 79pub struct GetDevicePayload {
36 id: String, 80 id: String,
37} 81}
38 82
83#[utoipa::path(
84 put,
85 path = "/device",
86 request_body = PutDevicePayload,
87 responses(
88 (status = 200, description = "List matching todos by query", body = [DeviceSchema])
89 ),
90 security(("api_key" = []))
91)]
39pub async fn put( 92pub async fn put(
40 State(state): State<Arc<crate::AppState>>, 93 State(state): State<Arc<crate::AppState>>,
41 Json(payload): Json<PutDevicePayload>, 94 Json(payload): Json<PutDevicePayload>,
@@ -44,26 +97,28 @@ pub async fn put(
44 "add device {} ({}, {}, {})", 97 "add device {} ({}, {}, {})",
45 payload.id, payload.mac, payload.broadcast_addr, payload.ip 98 payload.id, payload.mac, payload.broadcast_addr, payload.ip
46 ); 99 );
47 100
48 let ip = IpNetwork::from_str(&payload.ip)?; 101 let ip = IpNetwork::from_str(&payload.ip)?;
49 let mac = MacAddress::from_str(&payload.mac)?; 102 let mac = MacAddress::from_str(&payload.mac)?;
50 sqlx::query!( 103 let device = sqlx::query_as!(
104 Device,
51 r#" 105 r#"
52 INSERT INTO devices (id, mac, broadcast_addr, ip) 106 INSERT INTO devices (id, mac, broadcast_addr, ip)
53 VALUES ($1, $2, $3, $4); 107 VALUES ($1, $2, $3, $4)
108 RETURNING id, mac, broadcast_addr, ip, times;
54 "#, 109 "#,
55 payload.id, 110 payload.id,
56 mac, 111 mac,
57 payload.broadcast_addr, 112 payload.broadcast_addr,
58 ip 113 ip
59 ) 114 )
60 .execute(&state.db) 115 .fetch_one(&state.db)
61 .await?; 116 .await?;
62 117
63 Ok(Json(json!(PutDeviceResponse { success: true }))) 118 Ok(Json(json!(device)))
64} 119}
65 120
66#[derive(Deserialize)] 121#[derive(Deserialize, ToSchema)]
67pub struct PutDevicePayload { 122pub struct PutDevicePayload {
68 id: String, 123 id: String,
69 mac: String, 124 mac: String,
@@ -71,11 +126,15 @@ pub struct PutDevicePayload {
71 ip: String, 126 ip: String,
72} 127}
73 128
74#[derive(Serialize)] 129#[utoipa::path(
75pub struct PutDeviceResponse { 130 post,
76 success: bool, 131 path = "/device",
77} 132 request_body = PostDevicePayload,
78 133 responses(
134 (status = 200, description = "List matching todos by query", body = [DeviceSchema])
135 ),
136 security(("api_key" = []))
137)]
79pub async fn post( 138pub async fn post(
80 State(state): State<Arc<crate::AppState>>, 139 State(state): State<Arc<crate::AppState>>,
81 Json(payload): Json<PostDevicePayload>, 140 Json(payload): Json<PostDevicePayload>,
@@ -104,7 +163,7 @@ pub async fn post(
104 Ok(Json(json!(device))) 163 Ok(Json(json!(device)))
105} 164}
106 165
107#[derive(Deserialize)] 166#[derive(Deserialize, ToSchema)]
108pub struct PostDevicePayload { 167pub struct PostDevicePayload {
109 id: String, 168 id: String,
110 mac: String, 169 mac: String,
diff --git a/src/routes/start.rs b/src/routes/start.rs
index d4c0802..ef6e8f2 100644
--- a/src/routes/start.rs
+++ b/src/routes/start.rs
@@ -6,10 +6,20 @@ use axum::extract::State;
6use axum::Json; 6use axum::Json;
7use serde::{Deserialize, Serialize}; 7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value}; 8use serde_json::{json, Value};
9use utoipa::ToSchema;
9use std::sync::Arc; 10use std::sync::Arc;
10use tracing::{debug, info}; 11use tracing::{debug, info};
11use uuid::Uuid; 12use uuid::Uuid;
12 13
14#[utoipa::path(
15 post,
16 path = "/start",
17 request_body = Payload,
18 responses(
19 (status = 200, description = "List matching todos by query", body = [Response])
20 ),
21 security(("api_key" = []))
22)]
13pub async fn start( 23pub async fn start(
14 State(state): State<Arc<crate::AppState>>, 24 State(state): State<Arc<crate::AppState>>,
15 Json(payload): Json<Payload>, 25 Json(payload): Json<Payload>,
@@ -88,14 +98,14 @@ fn setup_ping(state: Arc<crate::AppState>, device: Device) -> String {
88 uuid_ret 98 uuid_ret
89} 99}
90 100
91#[derive(Deserialize)] 101#[derive(Deserialize, ToSchema)]
92pub struct Payload { 102pub struct Payload {
93 id: String, 103 id: String,
94 ping: Option<bool>, 104 ping: Option<bool>,
95} 105}
96 106
97#[derive(Serialize)] 107#[derive(Serialize, ToSchema)]
98struct Response { 108pub struct Response {
99 id: String, 109 id: String,
100 boot: bool, 110 boot: bool,
101 uuid: Option<String>, 111 uuid: Option<String>,
diff --git a/src/services/ping.rs b/src/services/ping.rs
index 9191f86..8cf6072 100644
--- a/src/services/ping.rs
+++ b/src/services/ping.rs
@@ -49,22 +49,27 @@ pub async fn spawn(
49 }; 49 };
50 } 50 }
51 51
52 trace!(?msg);
53
52 let msg = msg.expect("fatal error"); 54 let msg = msg.expect("fatal error");
53 55
54 let _ = tx.send(msg.clone()); 56 let _ = tx.send(msg.clone());
55 if let BroadcastCommands::Success = msg.command { 57 if msg.command == BroadcastCommands::Success {
56 sqlx::query!( 58 if timer.elapsed().whole_seconds() > config.pingthreshold {
57 r#" 59 sqlx::query!(
58 UPDATE devices 60 r#"
59 SET times = array_append(times, $1) 61 UPDATE devices
60 WHERE id = $2; 62 SET times = array_append(times, $1)
61 "#, 63 WHERE id = $2;
62 timer.elapsed().whole_seconds(), 64 "#,
63 device.id 65 timer.elapsed().whole_seconds(),
64 ) 66 device.id
65 .execute(db) 67 )
66 .await 68 .execute(db)
67 .unwrap(); 69 .await
70 .unwrap();
71 }
72
68 ping_map.insert( 73 ping_map.insert(
69 uuid.clone(), 74 uuid.clone(),
70 Value { 75 Value {