90 lines
3.2 KiB
Rust
90 lines
3.2 KiB
Rust
//! Generic, authorized reads of NATS KV data declared in content
|
|||
|
|
//! (`content::ResourceSpec`, on a `Feature`). The one property that
|
||
|
|
//! matters: the bucket/key a resource reads is never a client-supplied
|
||
|
|
//! parameter, only something the server resolves from its own
|
||
|
|
//! trusted, YAML-loaded content - the client only ever names a
|
||
|
|
//! question id + feature name, so it can't probe an arbitrary bucket
|
||
|
|
//! by just naming it.
|
||
|
|
|
||
|
|
use leptos::prelude::*;
|
||
|
|
|
||
|
|
/// Fetches the live data for `question_id`'s `feature_name` feature.
|
||
|
|
/// Fails closed: a resource with neither `public: true` nor
|
||
|
|
/// `requires_group` set is unreachable, not "open" by omission.
|
||
|
|
#[server]
|
||
|
|
pub async fn get_resource(
|
||
|
|
question_id: String,
|
||
|
|
feature_name: String,
|
||
|
|
) -> Result<serde_json::Value, ServerFnError> {
|
||
|
|
use crate::auth::{User, SESSION_USER_KEY};
|
||
|
|
use crate::server::AppState;
|
||
|
|
|
||
|
|
let state = expect_context::<AppState>();
|
||
|
|
let question = state
|
||
|
|
.questions
|
||
|
|
.get(&question_id)
|
||
|
|
.ok_or_else(|| ServerFnError::new("unknown question"))?;
|
||
|
|
let feature = question
|
||
|
|
.alternatives
|
||
|
|
.iter()
|
||
|
|
.flat_map(|a| &a.features)
|
||
|
|
.find(|f| f.name == feature_name)
|
||
|
|
.ok_or_else(|| ServerFnError::new("unknown feature"))?;
|
||
|
|
let resource = feature
|
||
|
|
.resource
|
||
|
|
.as_ref()
|
||
|
|
.ok_or_else(|| ServerFnError::new("feature has no resource"))?;
|
||
|
|
|
||
|
|
if !resource.public {
|
||
|
|
let group = resource
|
||
|
|
.requires_group
|
||
|
|
.as_deref()
|
||
|
|
.ok_or_else(|| ServerFnError::new("resource is not accessible"))?;
|
||
|
|
let session: tower_sessions::Session = leptos_axum::extract().await?;
|
||
|
|
let user = session
|
||
|
|
.get::<User>(SESSION_USER_KEY)
|
||
|
|
.await
|
||
|
|
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||
|
|
.ok_or_else(|| ServerFnError::new("not signed in"))?;
|
||
|
|
if !user.groups.iter().any(|g| g == group) {
|
||
|
|
return Err(ServerFnError::new("not authorized"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let store = state
|
||
|
|
.jetstream
|
||
|
|
.get_key_value(&resource.bucket)
|
||
|
|
.await
|
||
|
|
.map_err(|e| ServerFnError::new(format!("resource bucket unavailable: {e}")))?;
|
||
|
|
|
||
|
|
match &resource.key {
|
||
|
|
Some(key) => {
|
||
|
|
let bytes = store
|
||
|
|
.get(key)
|
||
|
|
.await
|
||
|
|
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||
|
|
.ok_or_else(|| ServerFnError::new("resource key not found"))?;
|
||
|
|
serde_json::from_slice(&bytes).map_err(|e| ServerFnError::new(e.to_string()))
|
||
|
|
}
|
||
|
|
None => {
|
||
|
|
use futures::TryStreamExt;
|
||
|
|
let keys: Vec<String> = store
|
||
|
|
.keys()
|
||
|
|
.await
|
||
|
|
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||
|
|
.try_collect()
|
||
|
|
.await
|
||
|
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||
|
|
let mut items = Vec::new();
|
||
|
|
for key in keys {
|
||
|
|
if let Ok(Some(bytes)) = store.get(&key).await {
|
||
|
|
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) {
|
||
|
|
items.push(value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(serde_json::Value::Array(items))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|