Simple pot-based access control

This commit is contained in:
Philip (a-0) 2024-01-21 12:24:05 +01:00
parent a768ce0f4e
commit d1b12e1562
15 changed files with 340 additions and 64 deletions

View file

@ -1,7 +1,7 @@
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::PotId; use crate::types::{PotId, Pot};
use super::UbisyncRequest; use super::UbisyncRequest;
@ -72,3 +72,24 @@ impl UbisyncRequest for AppSetDefaultPotRequest {
"/app/pot/default".to_string() "/app/pot/default".to_string()
} }
} }
#[derive(Serialize, Deserialize)]
pub struct AppGetDefaultPotRequest;
#[derive(Serialize, Deserialize)]
pub struct AppGetDefaultPotResponse {
pub pot: Pot,
}
impl UbisyncRequest for AppGetDefaultPotRequest {
type PathParameters = ();
type Response = AppGetDefaultPotResponse;
fn method(&self) -> Method {
Method::GET
}
fn path(&self, _: Self::PathParameters) -> String {
"/app/pot/default".to_string()
}
}

View file

@ -1,13 +1,14 @@
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::{Element, ElementContent, ElementId}; use crate::types::{Element, ElementContent, ElementId, PotId};
use super::UbisyncRequest; use super::UbisyncRequest;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct ElementCreateRequest { pub struct ElementCreateRequest {
pub content: ElementContent, pub content: ElementContent,
pub pot: Option<PotId>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::{ElementContent, ElementId, MessageId}; use crate::types::{ElementContent, ElementId, MessageId, PotId};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message { pub struct Message {
@ -22,6 +22,7 @@ pub enum MessageContent {
CreateElement { CreateElement {
id: ElementId, id: ElementId,
content: ElementContent, content: ElementContent,
pot: PotId,
}, },
SetElement { SetElement {
id: ElementId, id: ElementId,
@ -30,6 +31,10 @@ pub enum MessageContent {
RemoveElement { RemoveElement {
id: ElementId, id: ElementId,
}, },
AddPot {
id: PotId,
app_type: String,
}
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]

View file

@ -1,23 +1,26 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ElementContent, ElementId, MessageId}; use super::{ElementContent, ElementId, MessageId, PotId};
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Element { pub struct Element {
// Uuid identifying the element itself // Uuid identifying the element itself
id: ElementId, id: ElementId,
pot: Option<PotId>,
content: ElementContent, content: ElementContent,
latest_message: Option<MessageId>, latest_message: Option<MessageId>,
local_changes: bool, local_changes: bool,
} }
impl From<(ElementId, ElementContent, Option<MessageId>, bool)> for Element {
fn from(value: (ElementId, ElementContent, Option<MessageId>, bool)) -> Self { impl From<(ElementId, Option<PotId>, ElementContent, Option<MessageId>, bool)> for Element {
fn from(value: (ElementId, Option<PotId>, ElementContent, Option<MessageId>, bool)) -> Self {
Element { Element {
id: value.0, id: value.0,
content: value.1, pot: value.1,
latest_message: value.2, content: value.2,
local_changes: value.3, latest_message: value.3,
local_changes: value.4,
} }
} }
} }
@ -27,6 +30,7 @@ impl Element {
// A new element with no latest message must have local changes // A new element with no latest message must have local changes
Element { Element {
id: id, id: id,
pot: None,
content: content, content: content,
latest_message: None, latest_message: None,
local_changes: true, local_changes: true,

View file

@ -4,8 +4,8 @@ use super::PotId;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Pot { pub struct Pot {
id: PotId, pub id: PotId,
app_type: String, pub app_type: String,
} }
impl Pot { impl Pot {

View file

@ -13,7 +13,7 @@ use jsonwebtoken::{decode, Header};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::chrono::{DateTime, Utc}; use serde_with::chrono::{DateTime, Utc};
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use ubisync_lib::{api::app::{AppRegisterRequest, AppRegisterResponse, AppSetDefaultPotRequest, AppSetDefaultPotResponse, AppCreatePotRequest, AppCreatePotResponse}, types::PotId}; use ubisync_lib::{api::app::{AppRegisterRequest, AppRegisterResponse, AppSetDefaultPotRequest, AppSetDefaultPotResponse, AppCreatePotRequest, AppCreatePotResponse, AppGetDefaultPotRequest, AppGetDefaultPotResponse}, types::PotId};
use uuid::Uuid; use uuid::Uuid;
use crate::state::ApiState; use crate::state::ApiState;
@ -120,6 +120,20 @@ pub(super) async fn set_default_pot(
} }
} }
pub(super) async fn get_default_pot(
s: Extension<Arc<ApiState>>,
app_id: Extension<AppId>,
Json(_): Json<AppGetDefaultPotRequest>,
) -> Response {
match s.get_default_pot(&app_id.0) {
Ok(p) => (StatusCode::OK, Json {0: AppGetDefaultPotResponse { pot: p }}).into_response(),
Err(e) => {
warn!("No default pot found: {}", e);
StatusCode::NOT_FOUND.into_response()
}
}
}
pub(super) async fn create_pot( pub(super) async fn create_pot(
s: Extension<Arc<ApiState>>, s: Extension<Arc<ApiState>>,
app_id: Extension<AppId>, app_id: Extension<AppId>,

View file

@ -6,7 +6,7 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Extension, Extension,
}; };
use tracing::debug; use tracing::{debug, warn};
use crate::state::ApiState; use crate::state::ApiState;
use ubisync_lib::{ use ubisync_lib::{
@ -17,8 +17,10 @@ use ubisync_lib::{
types::ElementId, types::ElementId,
}; };
pub(super) async fn get(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response { use super::app::AppId;
let element = s.get_element(&id);
pub(super) async fn get(Path(id): Path<ElementId>, app: Extension<AppId>, s: Extension<Arc<ApiState>>) -> Response {
let element = s.get_element(&id, &app);
match element { match element {
Ok(el) => ( Ok(el) => (
StatusCode::OK, StatusCode::OK,
@ -27,15 +29,30 @@ pub(super) async fn get(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>)
}, },
) )
.into_response(), .into_response(),
Err(_) => StatusCode::NOT_FOUND.into_response(), Err(e) => {
warn!("Could not get element: {}", e);
StatusCode::NOT_FOUND.into_response()
},
} }
} }
pub(super) async fn create( pub(super) async fn create(
app: Extension<AppId>,
s: Extension<Arc<ApiState>>, s: Extension<Arc<ApiState>>,
Json(req): Json<ElementCreateRequest>, Json(req): Json<ElementCreateRequest>,
) -> Response { ) -> Response {
let element_id = s.create_element(&req.content); let pot_id = match req.pot {
Some(p) => p,
None => match s.get_default_pot(&app.0) {
Ok(p) => p.id,
Err(e) => {
warn!("Element create request did not provide pot id, and no default pot for requesting app was found: {}", e);
return StatusCode::INTERNAL_SERVER_ERROR.into_response()
},
},
};
let element_id = s.create_element(&req.content, &pot_id);
debug!("{:?}", element_id); debug!("{:?}", element_id);
match element_id { match element_id {
Ok(id) => ( Ok(id) => (
@ -51,10 +68,11 @@ pub(super) async fn create(
pub(super) async fn set( pub(super) async fn set(
Path(id): Path<ElementId>, Path(id): Path<ElementId>,
app: Extension<AppId>,
s: Extension<Arc<ApiState>>, s: Extension<Arc<ApiState>>,
Json(req): Json<ElementSetRequest>, Json(req): Json<ElementSetRequest>,
) -> Response { ) -> Response {
let res = s.write_element_content(&id, &req.content); let res = s.write_element_content(&id, &app, &req.content);
match res { match res {
Ok(_) => ( Ok(_) => (
StatusCode::OK, StatusCode::OK,

View file

@ -1,6 +1,6 @@
use axum::{ use axum::{
middleware::from_fn, middleware::from_fn,
routing::{get, put, post}, routing::{get, put},
Extension, Router, Extension, Router,
}; };
use std::sync::Arc; use std::sync::Arc;
@ -14,7 +14,7 @@ pub fn get_router(state: ApiState) -> Router {
Router::new() Router::new()
// authenticated routes // authenticated routes
.route("/app/pot", put(app::create_pot)) .route("/app/pot", put(app::create_pot))
.route("/app/pot/default", post(app::set_default_pot)) .route("/app/pot/default", get(app::get_default_pot).post(app::set_default_pot))
.route("/element", put(element::create)) .route("/element", put(element::create))
.route( .route(
"/element/:id", "/element/:id",

View file

@ -12,9 +12,9 @@ pub fn handle(state: &CommState, peer: &PeerId, message: Message) {
MessageContent::Hello { peer_name } => { MessageContent::Hello { peer_name } => {
state.set_peer(peer, peer_name).expect("State failed"); state.set_peer(peer, peer_name).expect("State failed");
} }
MessageContent::CreateElement { id, content } => { MessageContent::CreateElement { id, content , pot} => {
state state
.add_received_element(id, content, message.id()) .add_received_element(id, content, message.id(), pot)
.expect("State failed"); .expect("State failed");
} }
MessageContent::SetElement { id, content } => { MessageContent::SetElement { id, content } => {
@ -24,6 +24,11 @@ pub fn handle(state: &CommState, peer: &PeerId, message: Message) {
} }
MessageContent::RemoveElement { id } => { MessageContent::RemoveElement { id } => {
state.remove_element(id).expect("State failed"); state.remove_element(id).expect("State failed");
},
MessageContent::AddPot { id, app_type } => {
state.add_pot(id, app_type).expect("State failed");
//TODO: remove when setting default pot properly is possible
let _ = state.set_default_pot_for_all_apps(id);
} }
} }
} }

View file

@ -1,12 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Error;
use cozo::DbInstance; use cozo::DbInstance;
use jsonwebtoken::{DecodingKey, EncodingKey, Validation}; use jsonwebtoken::{DecodingKey, EncodingKey, Validation};
use serde_with::chrono::Utc; use serde_with::chrono::Utc;
use tracing::debug; use tracing::debug;
use ubisync_lib::{ use ubisync_lib::{
messages::MessageContent, messages::MessageContent,
types::{Element, ElementContent, ElementId, PotId}, types::{Element, ElementContent, ElementId, PotId, Pot},
}; };
use crate::{ use crate::{
@ -52,14 +53,19 @@ impl ApiState {
queries::apps::get(self.db(), id) queries::apps::get(self.db(), id)
} }
pub fn create_element(&self, content: &ElementContent) -> anyhow::Result<ElementId> { pub fn create_element(
&self,
content: &ElementContent,
pot: &PotId,
) -> anyhow::Result<ElementId> {
let id = ElementId::new(); let id = ElementId::new();
queries::elements::add(self.db(), &id, &content, None, true)?; queries::elements::add(self.db(), &id, &content, None, true, pot)?;
debug!("Added element {{{}}}", &id.to_string()); debug!("Added element {{{}}}", &id.to_string());
self.state.send_to_peers(MessageContent::CreateElement { self.state.send_to_peers(MessageContent::CreateElement {
id: id.clone(), id: id.clone(),
content: content.clone(), content: content.clone(),
pot: pot.clone(),
}); });
Ok(id) Ok(id)
} }
@ -67,13 +73,21 @@ impl ApiState {
pub fn write_element_content( pub fn write_element_content(
&self, &self,
id: &ElementId, id: &ElementId,
app: &AppId,
content: &ElementContent, content: &ElementContent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
queries::elements::set_content(self.db(), id, content)?; if queries::elements::get_app_access(self.db(), id, app)? {
queries::elements::set_local_changes(self.db(), id, true)?; queries::elements::set_content(self.db(), id, content)?;
debug!("Wrote element content {{{}}}", &id.to_string()); queries::elements::set_local_changes(self.db(), id, true)?;
debug!("Wrote element content {{{}}}", &id.to_string());
Ok(()) Ok(())
}
else {
Err(Error::msg(
"Element does not exist or app does not have access to it",
))
}
} }
pub fn remove_element(&self, id: &ElementId) -> anyhow::Result<()> { pub fn remove_element(&self, id: &ElementId) -> anyhow::Result<()> {
@ -87,14 +101,26 @@ impl ApiState {
queries::apps::set_default_pot(self.db(), app_id, pot_id) queries::apps::set_default_pot(self.db(), app_id, pot_id)
} }
pub fn get_default_pot(&self, app_id: &AppId) -> anyhow::Result<Pot> {
queries::apps::get_default_pot(self.db(), app_id)
}
pub fn create_pot(&self, app_id: &AppId, app_type: &str) -> anyhow::Result<PotId> { pub fn create_pot(&self, app_id: &AppId, app_type: &str) -> anyhow::Result<PotId> {
let pot_id = PotId::new(); let pot_id = PotId::new();
queries::apps::create_pot(self.db(), &pot_id, app_id, app_type)?; queries::apps::create_pot(self.db(), &pot_id, app_id, app_type)?;
self.state.send_to_peers(MessageContent::AddPot { id: pot_id.to_owned(), app_type: app_type.to_string() });
Ok(pot_id) Ok(pot_id)
} }
pub fn get_element(&self, id: &ElementId) -> anyhow::Result<Element> { pub fn get_element(&self, id: &ElementId, app: &AppId) -> anyhow::Result<Element> {
self.state.get_element(id) if queries::elements::get_app_access(self.db(), id, app)? {
self.state.get_element(id)
} else {
Err(Error::msg(
"Element does not exist or app does not have access to it",
))
}
} }
fn db(&self) -> &DbInstance { fn db(&self) -> &DbInstance {
@ -135,10 +161,12 @@ mod tests {
State::new("mem").await.unwrap(), State::new("mem").await.unwrap(),
"abcdabcdabcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcdabcdabcd",
); );
let app_id = state.add_app("appname", "appdesc", "apptype").unwrap();
let pot_id = state.create_pot(&app_id, "apptype").unwrap();
let id = state let id = state
.create_element(&ElementContent::Text("Test-text".to_string())) .create_element(&ElementContent::Text("Test-text".to_string()), &pot_id)
.unwrap(); .unwrap();
let el = state.get_element(&id).unwrap(); let el = state.get_element(&id, &app_id).unwrap();
assert_eq!( assert_eq!(
ElementContent::Text("Test-text".to_string()), ElementContent::Text("Test-text".to_string()),
el.content().to_owned() el.content().to_owned()
@ -157,13 +185,15 @@ mod tests {
State::new("mem").await.unwrap(), State::new("mem").await.unwrap(),
"abcdabcdabcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcdabcdabcd",
); );
let app_id = state.add_app("appname", "appdesc", "apptype").unwrap();
let pot_id = state.create_pot(&app_id, "apptype").unwrap();
let id = state let id = state
.create_element(&ElementContent::Text("Test-text".to_string())) .create_element(&ElementContent::Text("Test-text".to_string()), &pot_id)
.unwrap(); .unwrap();
state state
.write_element_content(&id, &ElementContent::Text("Test-text 2".to_string())) .write_element_content(&id, &app_id, &ElementContent::Text("Test-text 2".to_string()))
.unwrap(); .unwrap();
let el = state.get_element(&id).unwrap(); let el = state.get_element(&id, &app_id).unwrap();
assert_eq!( assert_eq!(
ElementContent::Text("Test-text 2".to_string()), ElementContent::Text("Test-text 2".to_string()),
el.content().to_owned() el.content().to_owned()

View file

@ -1,11 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Error;
use cozo::DbInstance; use cozo::DbInstance;
use tracing::debug; use tracing::debug;
use ubisync_lib::{ use ubisync_lib::{
peer::Peer, peer::Peer,
types::{Element, ElementContent, ElementId, MessageId, PeerId}, types::{Element, ElementContent, ElementId, MessageId, PeerId, PotId},
}; };
use crate::state::queries; use crate::state::queries;
@ -27,6 +28,7 @@ impl CommState {
id: &ElementId, id: &ElementId,
content: &ElementContent, content: &ElementContent,
latest_message: &MessageId, latest_message: &MessageId,
pot_id: &PotId,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
queries::elements::add( queries::elements::add(
self.db(), self.db(),
@ -34,6 +36,7 @@ impl CommState {
&content, &content,
Some(latest_message.to_owned()), Some(latest_message.to_owned()),
false, false,
pot_id,
)?; )?;
debug!("Added element {{{}}}", &id.to_string()); debug!("Added element {{{}}}", &id.to_string());
@ -76,6 +79,25 @@ impl CommState {
self.state.get_peers() self.state.get_peers()
} }
pub fn add_pot(&self, id: &PotId, app_type: &str) -> anyhow::Result<()> {
queries::pots::add(self.db(), id, app_type)
}
pub fn set_default_pot_for_all_apps(&self, id: &PotId) -> anyhow::Result<()> {
if let Ok(apps) = queries::apps::get_all_ids(self.db()) {
for app in apps {
let res = queries::apps::set_default_pot(self.db(), &app, id);
if let Ok(_) = res {
debug!("Set {:?} as default for app {:?}", id, &app);
}
}
Ok(())
}
else {
Err(Error::msg("Could not fetch list of all apps"))
}
}
fn db(&self) -> &DbInstance { fn db(&self) -> &DbInstance {
&self.state.db &self.state.db
} }
@ -88,7 +110,7 @@ mod tests {
use super::CommState; use super::CommState;
use tracing::Level; use tracing::Level;
use ubisync_lib::types::{ElementContent, ElementId, MessageId}; use ubisync_lib::types::{ElementContent, ElementId, MessageId, PotId};
#[tokio::test] #[tokio::test]
#[serial_test::serial] #[serial_test::serial]
@ -100,11 +122,13 @@ mod tests {
let state = CommState::new(State::new("mem").await.unwrap()); let state = CommState::new(State::new("mem").await.unwrap());
let id = ElementId::new(); let id = ElementId::new();
let pot_id = PotId::new();
state state
.add_received_element( .add_received_element(
&id, &id,
&ElementContent::Text("Test-text".to_string()), &ElementContent::Text("Test-text".to_string()),
&MessageId::new(), &MessageId::new(),
&pot_id,
) )
.unwrap(); .unwrap();
let el = state.get_element(&id).unwrap(); let el = state.get_element(&id).unwrap();
@ -124,11 +148,13 @@ mod tests {
let state = CommState::new(State::new("mem").await.unwrap()); let state = CommState::new(State::new("mem").await.unwrap());
let id = ElementId::new(); let id = ElementId::new();
let pot_id = PotId::new();
state state
.add_received_element( .add_received_element(
&id, &id,
&ElementContent::Text("Test-text".to_string()), &ElementContent::Text("Test-text".to_string()),
&MessageId::new(), &MessageId::new(),
&pot_id,
) )
.unwrap(); .unwrap();
state state

View file

@ -3,9 +3,13 @@ use std::collections::BTreeMap;
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use cozo::{DataValue, DbInstance, Num, ScriptMutability}; use cozo::{DataValue, DbInstance, Num, ScriptMutability};
use serde_with::chrono::{DateTime, Utc}; use serde_with::chrono::{DateTime, Utc};
use ubisync_lib::types::PotId; use tracing::warn;
use ubisync_lib::types::{Pot, PotId};
use crate::{api::v0::app::{AppId, App}, run_query}; use crate::{
api::v0::app::{App, AppId},
run_query,
};
pub fn add( pub fn add(
db: &DbInstance, db: &DbInstance,
@ -61,6 +65,30 @@ pub fn exists(db: &DbInstance, id: &AppId) -> anyhow::Result<bool> {
Err(Error::msg("Could not check whether app is registered")) Err(Error::msg("Could not check whether app is registered"))
} }
pub fn get_all_ids(db: &DbInstance) -> anyhow::Result<Vec<AppId>> {
let result = db.run_script(
"?[id] := *apps{id}",
BTreeMap::new(),
ScriptMutability::Immutable,
);
match result {
Ok(named_rows) => Ok(named_rows
.rows
.iter()
.filter_map(|row| {
if let [DataValue::Str(app_id)] = row.as_slice() {
serde_json::from_str::<AppId>(app_id).ok()
}
else {
None
}
})
.collect()),
Err(e) => bail!(e),
}
}
pub fn get(db: &DbInstance, id: &AppId) -> anyhow::Result<App> { pub fn get(db: &DbInstance, id: &AppId) -> anyhow::Result<App> {
let mut params = BTreeMap::new(); let mut params = BTreeMap::new();
params.insert( params.insert(
@ -76,13 +104,23 @@ pub fn get(db: &DbInstance, id: &AppId) -> anyhow::Result<App> {
if let Ok(rows) = result { if let Ok(rows) = result {
if let Some(firstrow) = rows.rows.first() { if let Some(firstrow) = rows.rows.first() {
if let [DataValue::Num(Num::Int(ts)), DataValue::Str(app_type), DataValue::Str(name), DataValue::Str(desc), default_pot] = firstrow.as_slice() { if let [DataValue::Num(Num::Int(ts)), DataValue::Str(app_type), DataValue::Str(name), DataValue::Str(desc), default_pot] =
let last_access = DateTime::from_timestamp(ts.to_owned(), 0).ok_or(Error::msg("Failed to deserialize timestamp"))?; firstrow.as_slice()
{
let last_access = DateTime::from_timestamp(ts.to_owned(), 0)
.ok_or(Error::msg("Failed to deserialize timestamp"))?;
let pot_id = match default_pot { let pot_id = match default_pot {
DataValue::Str(dpid) => Some(serde_json::from_str(dpid)?), DataValue::Str(dpid) => Some(serde_json::from_str(dpid)?),
_ => None, _ => None,
}; };
return Ok(App {id: id.clone(), app_type: app_type.to_string(), last_access, name: name.to_string(), description: desc.to_string(), default_pot: pot_id}); return Ok(App {
id: id.clone(),
app_type: app_type.to_string(),
last_access,
name: name.to_string(),
description: desc.to_string(),
default_pot: pot_id,
});
} }
} }
} }
@ -93,35 +131,124 @@ pub fn get(db: &DbInstance, id: &AppId) -> anyhow::Result<App> {
pub fn set_default_pot(db: &DbInstance, id: &AppId, pot: &PotId) -> anyhow::Result<()> { pub fn set_default_pot(db: &DbInstance, id: &AppId, pot: &PotId) -> anyhow::Result<()> {
let params = vec![ let params = vec![
("id", DataValue::Str(serde_json::to_string(id)?.into())), ("id", DataValue::Str(serde_json::to_string(id)?.into())),
("default_pot", DataValue::Str(serde_json::to_string(pot)?.into())) (
"default_pot",
DataValue::Str(serde_json::to_string(pot)?.into()),
),
]; ];
match run_query!( match run_query!(
&db, &db, ":update apps {id => default_pot}",
":update apps {id => default_pot}", params.clone(),
params,
ScriptMutability::Mutable ScriptMutability::Mutable
) { ) {
Ok(_) => Ok(()), Ok(_) => match run_query!(&db, ":put pot_memberships {pot_id = default_pot, app_id = id}", params, ScriptMutability::Mutable) {
Ok(_) => Ok(()),
Err(report) => bail!(report),
},
Err(report) => bail!(report), Err(report) => bail!(report),
} }
} }
pub fn create_pot(db: &DbInstance, pot_id: &PotId, app_id: &AppId, app_type: &str) -> anyhow::Result<()> { pub fn get_default_pot(db: &DbInstance, app: &AppId) -> anyhow::Result<Pot> {
let mut params = BTreeMap::new();
params.insert(
"app_id".to_string(),
DataValue::Str(serde_json::to_string(&app)?.into()),
);
let result = db.run_script(
"
default_pot[pot_id] := *apps{id: $app_id, default_pot: pot_id}
?[pot_id, app_type] := default_pot[pot_id], *pots[pot_id, app_type]
",
params.clone(),
ScriptMutability::Immutable,
);
match result {
Ok(rows) => {
if let Some(firstrow) = rows.rows.first() {
if let [DataValue::Str(pot_id_str), DataValue::Str(app_type)] = firstrow.as_slice()
{
Ok(Pot {
id: serde_json::from_str(&pot_id_str)?,
app_type: app_type.to_string(),
})
} else {
Err(Error::msg("Failed to deserialize query result"))
}
} else {
Err(Error::msg("App not found"))
}
}
Err(e) => bail!(e),
}
}
pub fn create_pot(
db: &DbInstance,
pot_id: &PotId,
app_id: &AppId,
app_type: &str,
) -> anyhow::Result<()> {
let params = vec![ let params = vec![
("pot_id", DataValue::Str(serde_json::to_string(pot_id)?.into())), (
("app_id", DataValue::Str(serde_json::to_string(app_id)?.into())), "pot_id",
DataValue::Str(serde_json::to_string(pot_id)?.into()),
),
(
"app_id",
DataValue::Str(serde_json::to_string(app_id)?.into()),
),
("app_type", DataValue::Str(app_type.into())), ("app_type", DataValue::Str(app_type.into())),
]; ];
match run_query!( match run_query!(
&db, &db,
":insert pots {pot_id => app_type} ":insert pots {id = pot_id => app_type}"
:insert pot_memberships {pot_id => app_id}", ,
params, params.clone(),
ScriptMutability::Mutable ScriptMutability::Mutable
) { ) {
Ok(_) => Ok(()), Ok(_) => match run_query!(&db, ":insert pot_memberships {pot_id => app_id}", params, ScriptMutability::Mutable) {
Err(e) => bail!(e), Ok(_) => Ok(()),
Err(e) => {
warn!("{:?}", e);
bail!(e)
},
},
Err(e) => {
warn!("{:?}", e);
bail!(e)
},
}
}
#[cfg(test)]
mod tests {
use cozo::DbInstance;
use serde_with::chrono::Utc;
use tracing::{Level, debug};
use ubisync_lib::types::PotId;
use crate::{state::{schema, queries::pots}, api::v0::app::AppId};
#[test]
pub fn default_pot() {
tracing_subscriber::fmt()
.pretty()
.with_max_level(Level::DEBUG)
.init();
let db = DbInstance::new("mem", "", Default::default()).unwrap();
schema::add_schema(&db).unwrap();
let app_id = AppId::new();
let pot_id = PotId::new();
super::add(&db, &app_id, &Utc::now(), "name", "description", "app_type").unwrap();
pots::add(&db, &pot_id, "app_type").unwrap();
super::set_default_pot(&db, &app_id, &pot_id).unwrap();
debug!("Result: {:?}", super::get_default_pot(&db, &app_id));
} }
} }

View file

@ -7,10 +7,10 @@ use tracing::{debug, error};
use crate::{ use crate::{
run_query, run_query,
state::{Element, ElementContent, ElementId}, state::{Element, ElementContent, ElementId}, api::v0::app::AppId,
}; };
use ubisync_lib::types::{MessageId, Tag}; use ubisync_lib::types::{MessageId, Tag, PotId};
pub fn add( pub fn add(
db: &DbInstance, db: &DbInstance,
@ -18,6 +18,7 @@ pub fn add(
content: &ElementContent, content: &ElementContent,
latest_message: Option<MessageId>, latest_message: Option<MessageId>,
local_changes: bool, local_changes: bool,
pot: &PotId,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let params = vec![ let params = vec![
("id", DataValue::Str(serde_json::to_string(&id)?.into())), ("id", DataValue::Str(serde_json::to_string(&id)?.into())),
@ -33,11 +34,12 @@ pub fn add(
}, },
), ),
("local_changes", DataValue::Bool(local_changes)), ("local_changes", DataValue::Bool(local_changes)),
("pot", DataValue::Str(serde_json::to_string(pot)?.into())),
]; ];
match run_query!( match run_query!(
&db, &db,
":insert elements {id => content, latest_message, local_changes}", ":insert elements {id => content, latest_message, local_changes, pot}",
params, params,
ScriptMutability::Mutable ScriptMutability::Mutable
) { ) {
@ -103,17 +105,18 @@ pub fn get(db: &DbInstance, id: &ElementId) -> anyhow::Result<Element> {
); );
let result = db.run_script(" let result = db.run_script("
?[content, latest_message, local_changes] := *elements[$id, content, latest_message, local_changes] ?[content, latest_message, local_changes, pot] := *elements[$id, content, latest_message, local_changes, pot]
", params, cozo::ScriptMutability::Immutable); ", params, cozo::ScriptMutability::Immutable);
match result { match result {
Ok(val) => { Ok(val) => {
if let Some(firstrow) = val.rows.first() { if let Some(firstrow) = val.rows.first() {
debug!("db result: {:?}", &firstrow.as_slice()); debug!("db result: {:?}", &firstrow.as_slice());
if let [DataValue::Json(JsonData(content)), latest_message, DataValue::Bool(local_changes)] = if let [DataValue::Json(JsonData(content)), latest_message, DataValue::Bool(local_changes), DataValue::Str(pot_id)] =
firstrow.as_slice() firstrow.as_slice()
{ {
return Ok(Element::from(( return Ok(Element::from((
id.to_owned(), id.to_owned(),
Some(serde_json::from_str(pot_id)?),
serde_json::from_value(content.to_owned())?, serde_json::from_value(content.to_owned())?,
match latest_message { match latest_message {
DataValue::Str(s) => Some(serde_json::from_str(s)?), DataValue::Str(s) => Some(serde_json::from_str(s)?),
@ -131,6 +134,27 @@ pub fn get(db: &DbInstance, id: &ElementId) -> anyhow::Result<Element> {
} }
} }
pub fn get_app_access(db: &DbInstance, id: &ElementId, app: &AppId) -> anyhow::Result<bool> {
let mut params = BTreeMap::new();
params.insert(
"id".to_string(),
DataValue::Str(serde_json::to_string(&id)?.into()),
);
params.insert("app_id".to_string(), DataValue::Str(serde_json::to_string(&app)?.into()));
let result = db.run_script("
memberships[pot_id] := *pot_memberships{pot_id, app_id}
?[id] := memberships[pot_id], *elements{id, pot: pot_id}
", params.clone(), cozo::ScriptMutability::Immutable);
match result {
Ok(named_rows) => {
Ok(named_rows.rows.len() > 0)
},
Err(report) => bail!(report),
}
}
pub fn get_by_tag(db: &DbInstance, tag: &Tag) -> anyhow::Result<Vec<ElementId>> { pub fn get_by_tag(db: &DbInstance, tag: &Tag) -> anyhow::Result<Vec<ElementId>> {
let mut params = BTreeMap::new(); let mut params = BTreeMap::new();
params.insert( params.insert(

View file

@ -37,6 +37,7 @@ pub fn add_schema(db: &DbInstance) -> anyhow::Result<()> {
content: Json, content: Json,
latest_message: String?, latest_message: String?,
local_changes: Bool, local_changes: Bool,
pot: String,
}} }}
{:create tags { {:create tags {
tag: String, tag: String,

View file

@ -30,17 +30,17 @@ async fn two_nodes_element_creation() {
.await .await
.unwrap(); .unwrap();
let api_client2 = UbisyncClient::init("localhost", 9982, None, "App", "Long desc", "test-app-type") let api_client2 = UbisyncClient::init("localhost", 9982, None, "App", "Long desc", "test-app-type")
.await
.unwrap()
.create_default_pot()
.await .await
.unwrap(); .unwrap();
tokio::time::sleep(Duration::from_millis(5000)).await;
let test_element_content = ElementContent::Text("Text".to_string()); let test_element_content = ElementContent::Text("Text".to_string());
let create_resp = api_client1 let create_resp = api_client1
.send( .send(
ElementCreateRequest { ElementCreateRequest {
content: test_element_content.clone(), content: test_element_content.clone(),
pot: None,
}, },
(), (),
) )