use axum::{
extract::{Path, State},
Json,
};
use freshblu_core::{
device::{DeviceView, RegisterParams},
error::FreshBluError,
forwarder::ForwarderEvent,
message::DeviceEvent,
permissions::PermissionChecker,
subscription::SubscriptionType,
};
use serde_json::{json, Value};
use std::collections::HashMap;
use uuid::Uuid;
use super::AuthenticatedDevice;
use crate::{ApiError, AppState};
type ApiResult<T> = Result<Json<T>, ApiError>;
pub async fn register(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
Json(params): Json<RegisterParams>,
) -> ApiResult<Value> {
if !state.config.open_registration {
let has_auth = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(freshblu_core::auth::parse_basic_auth)
.is_some();
let has_legacy_auth =
headers.get("skynet_auth_uuid").is_some() && headers.get("skynet_auth_token").is_some();
if !has_auth && !has_legacy_auth {
return Err(FreshBluError::Forbidden.into());
}
}
let (device, plaintext_token) = state.store.register(params).await?;
let mut resp = serde_json::to_value(&device)
.map_err(|e| ApiError::from(FreshBluError::Internal(e.to_string())))?;
resp["token"] = Value::String(plaintext_token);
Ok(Json(resp))
}
pub async fn get_device(
State(state): State<AppState>,
AuthenticatedDevice(actor, as_uuid): AuthenticatedDevice,
Path(uuid): Path<Uuid>,
) -> ApiResult<DeviceView> {
if let Some(ref as_u) = as_uuid {
let as_device = state
.store
.get_device(as_u)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
let checker = PermissionChecker::new(&as_device.meshblu.whitelists, &actor.uuid, as_u);
if !checker.can_discover_as() {
return Err(FreshBluError::Forbidden.into());
}
}
let device = state
.store
.get_device(&uuid)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
let effective_actor = as_uuid.unwrap_or(actor.uuid);
let checker = PermissionChecker::new(&device.meshblu.whitelists, &effective_actor, &uuid);
if !checker.can_discover_view() {
return Err(FreshBluError::NotFound.into());
}
Ok(Json(device.to_view()))
}
pub async fn update_device(
State(state): State<AppState>,
AuthenticatedDevice(actor, as_uuid): AuthenticatedDevice,
Path(uuid): Path<Uuid>,
Json(body): Json<HashMap<String, Value>>,
) -> ApiResult<DeviceView> {
if let Some(ref as_u) = as_uuid {
let as_device = state
.store
.get_device(as_u)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
let checker = PermissionChecker::new(&as_device.meshblu.whitelists, &actor.uuid, as_u);
if !checker.can_configure_as() {
return Err(FreshBluError::Forbidden.into());
}
}
let device = state
.store
.get_device(&uuid)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
let effective_actor = as_uuid.unwrap_or(actor.uuid);
let checker = PermissionChecker::new(&device.meshblu.whitelists, &effective_actor, &uuid);
if !checker.can_configure_update() {
return Err(FreshBluError::Forbidden.into());
}
let updated = state.store.update_device(&uuid, body).await?;
let view = updated.to_view();
let config_event = DeviceEvent::Config {
device: Box::new(view.clone()),
};
let subscribers = state
.store
.get_subscribers(&uuid, &SubscriptionType::ConfigureSent)
.await
.unwrap_or_default();
for sub_uuid in subscribers {
let _ = state.bus.publish(&sub_uuid, config_event.clone()).await;
}
let _ = state.bus.publish(&uuid, config_event).await;
let payload = serde_json::to_value(&view).unwrap_or_default();
let executor = state.webhook_executor.clone();
let dev = updated.clone();
tokio::spawn(async move {
executor
.execute(&dev, ForwarderEvent::ConfigureSent, &payload, &[])
.await;
});
Ok(Json(view))
}
pub async fn unregister(
State(state): State<AppState>,
AuthenticatedDevice(actor, as_uuid): AuthenticatedDevice,
Path(uuid): Path<Uuid>,
) -> ApiResult<Value> {
if let Some(ref as_u) = as_uuid {
let as_device = state
.store
.get_device(as_u)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
let checker = PermissionChecker::new(&as_device.meshblu.whitelists, &actor.uuid, as_u);
if !checker.can_configure_as() {
return Err(FreshBluError::Forbidden.into());
}
}
let device = state
.store
.get_device(&uuid)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
let effective_actor = as_uuid.unwrap_or(actor.uuid);
let checker = PermissionChecker::new(&device.meshblu.whitelists, &effective_actor, &uuid);
if !checker.can_configure_update() {
return Err(FreshBluError::Forbidden.into());
}
let unreg_event = DeviceEvent::Unregistered { uuid };
let subs = state
.store
.get_subscribers(&uuid, &SubscriptionType::UnregisterSent)
.await
.unwrap_or_default();
for sub_uuid in subs {
let _ = state.bus.publish(&sub_uuid, unreg_event.clone()).await;
}
let payload = json!({ "uuid": uuid });
let executor = state.webhook_executor.clone();
let dev = device.clone();
tokio::spawn(async move {
executor
.execute(&dev, ForwarderEvent::UnregisterSent, &payload, &[])
.await;
});
state.store.unregister(&uuid).await?;
state.bus.disconnect(&uuid);
Ok(Json(json!({ "uuid": uuid })))
}
pub async fn search(
State(state): State<AppState>,
AuthenticatedDevice(actor, as_uuid): AuthenticatedDevice,
Json(query): Json<HashMap<String, Value>>,
) -> ApiResult<Vec<DeviceView>> {
if let Some(ref as_u) = as_uuid {
let as_device = state
.store
.get_device(as_u)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
let checker = PermissionChecker::new(&as_device.meshblu.whitelists, &actor.uuid, as_u);
if !checker.can_discover_as() {
return Err(FreshBluError::Forbidden.into());
}
}
let effective_actor = as_uuid.unwrap_or(actor.uuid);
let all = state.store.search_devices(&query).await?;
let visible: Vec<DeviceView> = all
.into_iter()
.filter(|d| {
let checker = PermissionChecker::new(&d.meshblu.whitelists, &effective_actor, &d.uuid);
checker.can_discover_view()
})
.collect();
Ok(Json(visible))
}
pub async fn whoami(
State(state): State<AppState>,
AuthenticatedDevice(actor, _): AuthenticatedDevice,
) -> ApiResult<DeviceView> {
let device = state
.store
.get_device(&actor.uuid)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
Ok(Json(device.to_view()))
}
pub async fn my_devices(
State(state): State<AppState>,
AuthenticatedDevice(actor, _): AuthenticatedDevice,
) -> ApiResult<Vec<DeviceView>> {
let device = state
.store
.get_device(&actor.uuid)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
Ok(Json(vec![device.to_view()]))
}
pub async fn claim_device(
State(state): State<AppState>,
AuthenticatedDevice(actor, _): AuthenticatedDevice,
Path(uuid): Path<Uuid>,
) -> ApiResult<Value> {
let _device = state.store.claim_device(&uuid, &actor.uuid).await?;
Ok(Json(json!({ "uuid": uuid, "owner": actor.uuid })))
}
pub async fn get_public_key(
State(state): State<AppState>,
Path(uuid): Path<Uuid>,
) -> ApiResult<Value> {
let device = state
.store
.get_device(&uuid)
.await?
.ok_or(FreshBluError::NotFound)
.map_err(ApiError::from)?;
Ok(Json(json!({
"uuid": uuid,
"publicKey": device.meshblu.public_key,
})))
}
pub mod auth {
use super::*;
pub async fn authenticate(
State(state): State<AppState>,
Json(body): Json<HashMap<String, Value>>,
) -> ApiResult<Value> {
let uuid_str = body
.get("uuid")
.and_then(|v| v.as_str())
.ok_or_else(|| ApiError::from(FreshBluError::Validation("uuid required".into())))?;
let token = body
.get("token")
.and_then(|v| v.as_str())
.ok_or_else(|| ApiError::from(FreshBluError::Validation("token required".into())))?;
let uuid = Uuid::parse_str(uuid_str)
.map_err(|_| ApiError::from(FreshBluError::Validation("invalid uuid".into())))?;
let device = state
.store
.authenticate(&uuid, token)
.await?
.ok_or_else(|| ApiError::from(FreshBluError::Unauthorized))?;
Ok(Json(json!({ "uuid": device.uuid })))
}
}