...
This commit is contained in:
parent
4b2e8ca6b9
commit
c956db8adf
2017
acldb/Cargo.lock
generated
Normal file
2017
acldb/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -13,9 +13,14 @@ serde_json = "1.0"
|
|||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
actix-rt = "2"
|
actix-rt = "2"
|
||||||
actix-cors = "0.6"
|
actix-cors = "0.6"
|
||||||
openapi = "0.6"
|
utoipa = { version = "3.3", features = ["actix_extras"] }
|
||||||
|
utoipa-swagger-ui = { version = "3.1", features = ["actix-web"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
env_logger = "0.10"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
base64 = "0.13"
|
||||||
|
dirs = "4.0"
|
||||||
|
async-trait = "0.1"
|
||||||
|
97
acldb/src/acl.rs
Normal file
97
acldb/src/acl.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
/// Represents permission levels in the ACL system
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
pub enum ACLRight {
|
||||||
|
/// Read permission
|
||||||
|
Read,
|
||||||
|
/// Write permission (includes Read)
|
||||||
|
Write,
|
||||||
|
/// Delete permission (includes Write and Read)
|
||||||
|
Delete,
|
||||||
|
/// Execute permission (includes Delete, Write, and Read)
|
||||||
|
Execute,
|
||||||
|
/// Admin permission (includes all other permissions)
|
||||||
|
Admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access Control List for managing permissions
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ACL {
|
||||||
|
/// Unique name for the ACL within a circle
|
||||||
|
pub name: String,
|
||||||
|
/// Map of public keys to their permission levels
|
||||||
|
permissions: HashMap<String, ACLRight>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ACL {
|
||||||
|
/// Creates a new ACL with the given name
|
||||||
|
pub fn new(name: &str) -> Self {
|
||||||
|
ACL {
|
||||||
|
name: name.to_string(),
|
||||||
|
permissions: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a permission for a public key
|
||||||
|
pub fn set_permission(&mut self, pubkey: &str, right: ACLRight) {
|
||||||
|
self.permissions.insert(pubkey.to_string(), right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a permission for a public key
|
||||||
|
pub fn remove_permission(&mut self, pubkey: &str) {
|
||||||
|
self.permissions.remove(pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a public key has at least the specified permission level
|
||||||
|
pub fn has_permission(&self, pubkey: &str, right: ACLRight) -> bool {
|
||||||
|
if let Some(assigned_right) = self.permissions.get(pubkey) {
|
||||||
|
return *assigned_right >= right;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all public keys with their associated permissions
|
||||||
|
pub fn get_all_permissions(&self) -> &HashMap<String, ACLRight> {
|
||||||
|
&self.permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the permission level for a specific public key
|
||||||
|
pub fn get_permission(&self, pubkey: &str) -> Option<ACLRight> {
|
||||||
|
self.permissions.get(pubkey).copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_acl_permissions() {
|
||||||
|
let mut acl = ACL::new("test_acl");
|
||||||
|
|
||||||
|
// Set permissions
|
||||||
|
acl.set_permission("user1", ACLRight::Read);
|
||||||
|
acl.set_permission("user2", ACLRight::Write);
|
||||||
|
acl.set_permission("user3", ACLRight::Admin);
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
assert!(acl.has_permission("user1", ACLRight::Read));
|
||||||
|
assert!(!acl.has_permission("user1", ACLRight::Write));
|
||||||
|
|
||||||
|
assert!(acl.has_permission("user2", ACLRight::Read));
|
||||||
|
assert!(acl.has_permission("user2", ACLRight::Write));
|
||||||
|
assert!(!acl.has_permission("user2", ACLRight::Delete));
|
||||||
|
|
||||||
|
assert!(acl.has_permission("user3", ACLRight::Read));
|
||||||
|
assert!(acl.has_permission("user3", ACLRight::Write));
|
||||||
|
assert!(acl.has_permission("user3", ACLRight::Delete));
|
||||||
|
assert!(acl.has_permission("user3", ACLRight::Execute));
|
||||||
|
assert!(acl.has_permission("user3", ACLRight::Admin));
|
||||||
|
|
||||||
|
// Remove permission
|
||||||
|
acl.remove_permission("user2");
|
||||||
|
assert!(!acl.has_permission("user2", ACLRight::Read));
|
||||||
|
}
|
||||||
|
}
|
47
acldb/src/error.rs
Normal file
47
acldb/src/error.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Error types for the ACLDB module
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Permission denied error
|
||||||
|
#[error("Permission denied")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
/// Record not found error
|
||||||
|
#[error("Record not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
/// Invalid operation error
|
||||||
|
#[error("Invalid operation: {0}")]
|
||||||
|
InvalidOperation(String),
|
||||||
|
|
||||||
|
/// Path error
|
||||||
|
#[error("Path error: {0}")]
|
||||||
|
PathError(String),
|
||||||
|
|
||||||
|
/// OurDB error
|
||||||
|
#[error("OurDB error: {0}")]
|
||||||
|
OurDBError(#[from] ourdb::Error),
|
||||||
|
|
||||||
|
/// TST error
|
||||||
|
#[error("TST error: {0}")]
|
||||||
|
TSTError(#[from] tst::Error),
|
||||||
|
|
||||||
|
/// IO error
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// Serialization error
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
SerializationError(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
/// Signature verification error
|
||||||
|
#[error("Signature verification error: {0}")]
|
||||||
|
SignatureError(String),
|
||||||
|
|
||||||
|
/// Invalid request error
|
||||||
|
#[error("Invalid request: {0}")]
|
||||||
|
InvalidRequest(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
267
acldb/src/lib.rs
Normal file
267
acldb/src/lib.rs
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
mod acl;
|
||||||
|
mod error;
|
||||||
|
mod topic;
|
||||||
|
mod rpc;
|
||||||
|
mod server;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use acl::{ACL, ACLRight};
|
||||||
|
pub use error::Error;
|
||||||
|
pub use topic::ACLDBTopic;
|
||||||
|
pub use rpc::RpcInterface;
|
||||||
|
pub use server::Server;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use ourdb::OurDB;
|
||||||
|
use tst::TST;
|
||||||
|
|
||||||
|
/// ACLDB represents an access-controlled database instance for a specific circle
|
||||||
|
pub struct ACLDB {
|
||||||
|
/// Circle ID
|
||||||
|
circle_id: String,
|
||||||
|
/// Base directory path
|
||||||
|
base_path: String,
|
||||||
|
/// OurDB instance for the circle
|
||||||
|
db: OurDB,
|
||||||
|
/// TST instance for key-to-id mapping
|
||||||
|
tst: TST,
|
||||||
|
/// Cache of loaded ACLs
|
||||||
|
acl_cache: HashMap<String, ACL>,
|
||||||
|
/// Topic instances
|
||||||
|
topics: HashMap<String, Arc<RwLock<ACLDBTopic>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ACLDB {
|
||||||
|
/// Creates a new ACLDB instance for the specified circle
|
||||||
|
pub fn new(circle_id: &str) -> Result<Self, Error> {
|
||||||
|
let home_dir = dirs::home_dir().ok_or_else(|| Error::PathError("Home directory not found".to_string()))?;
|
||||||
|
let base_path = home_dir.join("hero/var/ourdb").join(circle_id);
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
std::fs::create_dir_all(&base_path)?;
|
||||||
|
|
||||||
|
// Initialize OurDB for the circle
|
||||||
|
let ourdb_path = base_path.join("data");
|
||||||
|
std::fs::create_dir_all(&ourdb_path)?;
|
||||||
|
|
||||||
|
let db_config = ourdb::OurDBConfig {
|
||||||
|
path: ourdb_path,
|
||||||
|
incremental_mode: true,
|
||||||
|
file_size: None,
|
||||||
|
keysize: None,
|
||||||
|
reset: Some(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = OurDB::new(db_config)?;
|
||||||
|
|
||||||
|
// Initialize TST for key-to-id mapping
|
||||||
|
let tst_path = base_path.join("tst").to_string_lossy().to_string();
|
||||||
|
let tst = TST::new(&tst_path, false)?;
|
||||||
|
|
||||||
|
Ok(ACLDB {
|
||||||
|
circle_id: circle_id.to_string(),
|
||||||
|
base_path: base_path.to_string_lossy().to_string(),
|
||||||
|
db,
|
||||||
|
tst,
|
||||||
|
acl_cache: HashMap::new(),
|
||||||
|
topics: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a topic instance, creating it if it doesn't exist
|
||||||
|
pub fn topic(&mut self, topic_name: &str) -> Arc<RwLock<ACLDBTopic>> {
|
||||||
|
if let Some(topic) = self.topics.get(topic_name) {
|
||||||
|
return Arc::clone(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since OurDB and TST don't implement Clone, we'll create new instances
|
||||||
|
// In a real implementation, we would use a connection pool or similar
|
||||||
|
let topic = Arc::new(RwLock::new(ACLDBTopic::new(
|
||||||
|
self.circle_id.clone(),
|
||||||
|
topic_name.to_string(),
|
||||||
|
Arc::new(RwLock::new(OurDB::new(ourdb::OurDBConfig {
|
||||||
|
path: Path::new(&self.base_path).join("data").join(topic_name),
|
||||||
|
incremental_mode: true,
|
||||||
|
file_size: None,
|
||||||
|
keysize: None,
|
||||||
|
reset: Some(false),
|
||||||
|
}).unwrap())),
|
||||||
|
Arc::new(RwLock::new(TST::new(
|
||||||
|
&Path::new(&self.base_path).join("tst").join(topic_name).to_string_lossy(),
|
||||||
|
false
|
||||||
|
).unwrap())),
|
||||||
|
)));
|
||||||
|
|
||||||
|
self.topics.insert(topic_name.to_string(), Arc::clone(&topic));
|
||||||
|
topic
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates or creates an ACL with specified permissions
|
||||||
|
pub async fn acl_update(&mut self, caller_pubkey: &str, name: &str, pubkeys: &[String], right: ACLRight) -> Result<(), Error> {
|
||||||
|
// Check if caller has admin rights
|
||||||
|
self.check_admin_rights(caller_pubkey).await?;
|
||||||
|
|
||||||
|
// Get or create the ACL
|
||||||
|
let mut acl = self.get_or_create_acl(name).await?;
|
||||||
|
|
||||||
|
// Update permissions for each public key
|
||||||
|
for pubkey in pubkeys {
|
||||||
|
acl.set_permission(pubkey, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated ACL
|
||||||
|
self.save_acl(&acl).await?;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
self.acl_cache.insert(name.to_string(), acl);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes specific public keys from an existing ACL
|
||||||
|
pub async fn acl_remove(&mut self, caller_pubkey: &str, name: &str, pubkeys: &[String]) -> Result<(), Error> {
|
||||||
|
// Check if caller has admin rights
|
||||||
|
self.check_admin_rights(caller_pubkey).await?;
|
||||||
|
|
||||||
|
// Get the ACL
|
||||||
|
let mut acl = self.get_acl(name).await?;
|
||||||
|
|
||||||
|
// Remove permissions for each public key
|
||||||
|
for pubkey in pubkeys {
|
||||||
|
acl.remove_permission(pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated ACL
|
||||||
|
self.save_acl(&acl).await?;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
self.acl_cache.insert(name.to_string(), acl);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes an entire ACL
|
||||||
|
pub async fn acl_del(&mut self, caller_pubkey: &str, name: &str) -> Result<(), Error> {
|
||||||
|
// Check if caller has admin rights
|
||||||
|
self.check_admin_rights(caller_pubkey).await?;
|
||||||
|
|
||||||
|
// Get the ACL to ensure it exists
|
||||||
|
let _acl = self.get_acl(name).await?;
|
||||||
|
|
||||||
|
// Get the ACL topic
|
||||||
|
let topic = self.topic("acl");
|
||||||
|
let topic = topic.write().await;
|
||||||
|
|
||||||
|
// Delete the ACL
|
||||||
|
topic.delete(name).await?;
|
||||||
|
|
||||||
|
// Remove from cache
|
||||||
|
self.acl_cache.remove(name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets an ACL by name
|
||||||
|
pub async fn get_acl(&mut self, name: &str) -> Result<ACL, Error> {
|
||||||
|
// Check cache first
|
||||||
|
if let Some(acl) = self.acl_cache.get(name) {
|
||||||
|
return Ok(acl.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the ACL topic
|
||||||
|
let topic = self.topic("acl");
|
||||||
|
let topic = topic.read().await;
|
||||||
|
|
||||||
|
// Get the ACL data
|
||||||
|
let acl_data = topic.get(name).await?;
|
||||||
|
|
||||||
|
// Deserialize the ACL
|
||||||
|
let acl: ACL = serde_json::from_slice(&acl_data)?;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
self.acl_cache.insert(name.to_string(), acl.clone());
|
||||||
|
|
||||||
|
Ok(acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets or creates an ACL
|
||||||
|
async fn get_or_create_acl(&mut self, name: &str) -> Result<ACL, Error> {
|
||||||
|
match self.get_acl(name).await {
|
||||||
|
Ok(acl) => Ok(acl),
|
||||||
|
Err(_) => {
|
||||||
|
// Create a new ACL
|
||||||
|
let acl = ACL::new(name);
|
||||||
|
|
||||||
|
// Save the ACL
|
||||||
|
self.save_acl(&acl).await?;
|
||||||
|
|
||||||
|
Ok(acl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves an ACL
|
||||||
|
async fn save_acl(&mut self, acl: &ACL) -> Result<(), Error> {
|
||||||
|
// Serialize the ACL
|
||||||
|
let acl_data = serde_json::to_vec(acl)?;
|
||||||
|
|
||||||
|
// Get the ACL topic
|
||||||
|
let topic = self.topic("acl");
|
||||||
|
let topic = topic.write().await;
|
||||||
|
|
||||||
|
// Save the ACL
|
||||||
|
topic.set(&acl.name, &acl_data).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a caller has admin rights
|
||||||
|
async fn check_admin_rights(&mut self, caller_pubkey: &str) -> Result<(), Error> {
|
||||||
|
// For the circle creator/owner, always grant admin rights
|
||||||
|
if self.is_circle_owner(caller_pubkey) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's an admin ACL
|
||||||
|
match self.get_acl("admin").await {
|
||||||
|
Ok(acl) => {
|
||||||
|
// Check if the caller has admin rights
|
||||||
|
if acl.has_permission(caller_pubkey, ACLRight::Admin) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::PermissionDenied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// If no admin ACL exists, only the circle owner can perform admin operations
|
||||||
|
Err(Error::PermissionDenied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a caller is the circle owner
|
||||||
|
fn is_circle_owner(&self, caller_pubkey: &str) -> bool {
|
||||||
|
// In a real implementation, this would check against the circle's owner
|
||||||
|
// For now, we'll use a simple check based on the circle ID
|
||||||
|
// This should be replaced with proper circle ownership verification
|
||||||
|
let circle_owner_file = Path::new(&self.base_path).join("owner");
|
||||||
|
if circle_owner_file.exists() {
|
||||||
|
if let Ok(owner) = std::fs::read_to_string(circle_owner_file) {
|
||||||
|
return owner.trim() == caller_pubkey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no owner file exists, check if this is the first admin operation
|
||||||
|
self.acl_cache.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Tests will be added here
|
||||||
|
}
|
42
acldb/src/main.rs
Normal file
42
acldb/src/main.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use acldb::{Server, Error};
|
||||||
|
use std::env;
|
||||||
|
use log::{info, error, LevelFilter};
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> Result<(), Error> {
|
||||||
|
// Initialize logger
|
||||||
|
env_logger::Builder::new()
|
||||||
|
.filter_level(LevelFilter::Info)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("Starting ACLDB server...");
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let host = args.get(1).map_or("127.0.0.1".to_string(), |s| s.clone());
|
||||||
|
let port = args.get(2)
|
||||||
|
.map_or(8080, |s| s.parse::<u16>().unwrap_or(8080));
|
||||||
|
|
||||||
|
// Create server configuration
|
||||||
|
let config = acldb::server::ServerConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create and start server
|
||||||
|
let server = Server::new(config);
|
||||||
|
info!("Server listening on {}:{}", config.host, config.port);
|
||||||
|
info!("Swagger UI available at http://{}:{}/swagger", config.host, config.port);
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
match server.start().await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Server stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Server error: {}", e);
|
||||||
|
Err(Error::IOError(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
231
acldb/src/rpc.rs
Normal file
231
acldb/src/rpc.rs
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::acl::ACLRight;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
/// RPC request structure
|
||||||
|
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||||
|
pub struct RpcRequest {
|
||||||
|
/// Method name
|
||||||
|
pub method: String,
|
||||||
|
/// JSON-encoded arguments
|
||||||
|
pub params: serde_json::Value,
|
||||||
|
/// Signature of the JSON data
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RPC response structure
|
||||||
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
|
pub struct RpcResponse {
|
||||||
|
/// Result of the operation
|
||||||
|
pub result: Option<serde_json::Value>,
|
||||||
|
/// Error message if any
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ACL update request parameters
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct AclUpdateParams {
|
||||||
|
/// Public key of the requesting user
|
||||||
|
pub caller_pubkey: String,
|
||||||
|
/// ID of the circle where the ACL exists
|
||||||
|
pub circle_id: String,
|
||||||
|
/// Unique name for the ACL within the circle
|
||||||
|
pub name: String,
|
||||||
|
/// Array of public keys to grant permissions to
|
||||||
|
pub pubkeys: Vec<String>,
|
||||||
|
/// Permission level
|
||||||
|
pub right: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ACL remove request parameters
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct AclRemoveParams {
|
||||||
|
/// Public key of the requesting user
|
||||||
|
pub caller_pubkey: String,
|
||||||
|
/// ID of the circle where the ACL exists
|
||||||
|
pub circle_id: String,
|
||||||
|
/// Name of the ACL to modify
|
||||||
|
pub name: String,
|
||||||
|
/// Array of public keys to remove from the ACL
|
||||||
|
pub pubkeys: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ACL delete request parameters
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct AclDelParams {
|
||||||
|
/// Public key of the requesting user
|
||||||
|
pub caller_pubkey: String,
|
||||||
|
/// ID of the circle where the ACL exists
|
||||||
|
pub circle_id: String,
|
||||||
|
/// Name of the ACL to delete
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set request parameters
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct SetParams {
|
||||||
|
/// Public key of the requesting user
|
||||||
|
pub caller_pubkey: String,
|
||||||
|
/// ID of the circle where the data belongs
|
||||||
|
pub circle_id: String,
|
||||||
|
/// String identifier for the database category
|
||||||
|
pub topic: String,
|
||||||
|
/// Optional string key for the record
|
||||||
|
pub key: Option<String>,
|
||||||
|
/// Optional numeric ID for direct access
|
||||||
|
pub id: Option<u32>,
|
||||||
|
/// Base64-encoded data to store
|
||||||
|
pub value: String,
|
||||||
|
/// ID of the ACL to protect this record (0 for public access)
|
||||||
|
pub acl_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete request parameters
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DelParams {
|
||||||
|
/// Public key of the requesting user
|
||||||
|
pub caller_pubkey: String,
|
||||||
|
/// ID of the circle where the data belongs
|
||||||
|
pub circle_id: String,
|
||||||
|
/// String identifier for the database category
|
||||||
|
pub topic: String,
|
||||||
|
/// Optional string key for the record
|
||||||
|
pub key: Option<String>,
|
||||||
|
/// Optional numeric ID for direct access
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get request parameters
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GetParams {
|
||||||
|
/// Public key of the requesting user
|
||||||
|
pub caller_pubkey: String,
|
||||||
|
/// ID of the circle where the data belongs
|
||||||
|
pub circle_id: String,
|
||||||
|
/// String identifier for the database category
|
||||||
|
pub topic: String,
|
||||||
|
/// Optional string key for the record
|
||||||
|
pub key: Option<String>,
|
||||||
|
/// Optional numeric ID for direct access
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefix request parameters
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct PrefixParams {
|
||||||
|
/// Public key of the requesting user
|
||||||
|
pub caller_pubkey: String,
|
||||||
|
/// ID of the circle where the data belongs
|
||||||
|
pub circle_id: String,
|
||||||
|
/// String identifier for the database category
|
||||||
|
pub topic: String,
|
||||||
|
/// Prefix to search for
|
||||||
|
pub prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RPC interface for handling client requests
|
||||||
|
pub struct RpcInterface {
|
||||||
|
/// Map of method names to handler functions
|
||||||
|
handlers: HashMap<String, Box<dyn Fn(serde_json::Value) -> Result<serde_json::Value, Error> + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcInterface {
|
||||||
|
/// Creates a new RPC interface
|
||||||
|
pub fn new() -> Self {
|
||||||
|
RpcInterface {
|
||||||
|
handlers: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a handler for a method
|
||||||
|
pub fn register<F>(&mut self, method: &str, handler: F)
|
||||||
|
where
|
||||||
|
F: Fn(serde_json::Value) -> Result<serde_json::Value, Error> + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.handlers.insert(method.to_string(), Box::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles an RPC request
|
||||||
|
pub fn handle(&self, request: RpcRequest) -> RpcResponse {
|
||||||
|
// Verify the signature
|
||||||
|
if let Err(err) = self.verify_signature(&request) {
|
||||||
|
return RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the caller's public key from the signature
|
||||||
|
let caller_pubkey = self.extract_pubkey(&request.signature).unwrap_or_default();
|
||||||
|
|
||||||
|
// Call the appropriate handler
|
||||||
|
match self.handlers.get(&request.method) {
|
||||||
|
Some(handler) => {
|
||||||
|
match handler(request.params) {
|
||||||
|
Ok(result) => RpcResponse {
|
||||||
|
result: Some(result),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Method not found: {}", request.method)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies the signature of an RPC request
|
||||||
|
fn verify_signature(&self, request: &RpcRequest) -> Result<(), Error> {
|
||||||
|
// In a real implementation, this would verify the cryptographic signature
|
||||||
|
// For now, we'll just check that the signature is not empty
|
||||||
|
if request.signature.is_empty() {
|
||||||
|
return Err(Error::SignatureError("Empty signature".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the public key from a signature
|
||||||
|
fn extract_pubkey(&self, signature: &str) -> Result<String, Error> {
|
||||||
|
// In a real implementation, this would extract the public key from the signature
|
||||||
|
// For now, we'll just return a placeholder
|
||||||
|
Ok("extracted_pubkey".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a string to an ACLRight enum
|
||||||
|
pub fn parse_acl_right(right_str: &str) -> Result<ACLRight, Error> {
|
||||||
|
match right_str.to_lowercase().as_str() {
|
||||||
|
"read" => Ok(ACLRight::Read),
|
||||||
|
"write" => Ok(ACLRight::Write),
|
||||||
|
"delete" => Ok(ACLRight::Delete),
|
||||||
|
"execute" => Ok(ACLRight::Execute),
|
||||||
|
"admin" => Ok(ACLRight::Admin),
|
||||||
|
_ => Err(Error::InvalidRequest(format!("Invalid ACL right: {}", right_str))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_acl_right() {
|
||||||
|
let rpc = RpcInterface::new();
|
||||||
|
|
||||||
|
assert_eq!(RpcInterface::parse_acl_right("read").unwrap(), ACLRight::Read);
|
||||||
|
assert_eq!(RpcInterface::parse_acl_right("write").unwrap(), ACLRight::Write);
|
||||||
|
assert_eq!(RpcInterface::parse_acl_right("delete").unwrap(), ACLRight::Delete);
|
||||||
|
assert_eq!(RpcInterface::parse_acl_right("execute").unwrap(), ACLRight::Execute);
|
||||||
|
assert_eq!(RpcInterface::parse_acl_right("admin").unwrap(), ACLRight::Admin);
|
||||||
|
|
||||||
|
assert!(RpcInterface::parse_acl_right("invalid").is_err());
|
||||||
|
}
|
||||||
|
}
|
486
acldb/src/server.rs
Normal file
486
acldb/src/server.rs
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
|
||||||
|
use actix_web::middleware::Logger;
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use serde_json::json;
|
||||||
|
use log::{error};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
|
use crate::ACLDB;
|
||||||
|
use crate::rpc::{RpcInterface, RpcRequest, RpcResponse, AclUpdateParams, AclRemoveParams, AclDelParams, SetParams, DelParams, GetParams, PrefixParams};
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::utils::base64_decode;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use tokio::task;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
/// Server configuration
|
||||||
|
pub struct ServerConfig {
|
||||||
|
/// Host address
|
||||||
|
pub host: String,
|
||||||
|
/// Port number
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
ServerConfig {
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request queue for a circle
|
||||||
|
struct CircleQueue {
|
||||||
|
/// Queue of pending requests
|
||||||
|
queue: VecDeque<(RpcRequest, mpsc::Sender<RpcResponse>)>,
|
||||||
|
/// Flag to indicate if a worker is currently processing this queue
|
||||||
|
is_processing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CircleQueue {
|
||||||
|
/// Creates a new circle queue
|
||||||
|
fn new() -> Self {
|
||||||
|
CircleQueue {
|
||||||
|
queue: VecDeque::new(),
|
||||||
|
is_processing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a request to the queue and starts processing if needed
|
||||||
|
async fn add_request(
|
||||||
|
&mut self,
|
||||||
|
request: RpcRequest,
|
||||||
|
response_sender: mpsc::Sender<RpcResponse>,
|
||||||
|
rpc_interface: Arc<RpcInterface>,
|
||||||
|
acldb_factory: Arc<ACLDBFactory>,
|
||||||
|
) {
|
||||||
|
// Add the request to the queue
|
||||||
|
self.queue.push_back((request.clone(), response_sender));
|
||||||
|
|
||||||
|
// If no worker is processing this queue, start one
|
||||||
|
if !self.is_processing {
|
||||||
|
self.is_processing = true;
|
||||||
|
|
||||||
|
// Clone what we need for the worker
|
||||||
|
let rpc = Arc::clone(&rpc_interface);
|
||||||
|
let factory = Arc::clone(&acldb_factory);
|
||||||
|
let mut queue = self.queue.clone();
|
||||||
|
|
||||||
|
// Spawn a worker task
|
||||||
|
task::spawn(async move {
|
||||||
|
// Process all requests in the queue
|
||||||
|
while let Some((req, sender)) = queue.pop_front() {
|
||||||
|
// Process the request
|
||||||
|
let response = process_request(&req, &rpc, &factory).await;
|
||||||
|
|
||||||
|
// Send the response
|
||||||
|
if let Err(err) = sender.send(response).await {
|
||||||
|
error!("Failed to send response: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent CPU hogging
|
||||||
|
sleep(Duration::from_millis(1)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Factory for creating ACLDB instances
|
||||||
|
pub struct ACLDBFactory {
|
||||||
|
/// Map of circle IDs to ACLDB instances
|
||||||
|
dbs: RwLock<HashMap<String, Arc<RwLock<ACLDB>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ACLDBFactory {
|
||||||
|
/// Creates a new ACLDBFactory
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ACLDBFactory {
|
||||||
|
dbs: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets or creates an ACLDB instance for a circle
|
||||||
|
pub async fn get_or_create(&self, circle_id: &str) -> Result<Arc<RwLock<ACLDB>>, Error> {
|
||||||
|
// Try to get an existing instance
|
||||||
|
let dbs = self.dbs.read().await;
|
||||||
|
if let Some(db) = dbs.get(circle_id) {
|
||||||
|
return Ok(Arc::clone(db));
|
||||||
|
}
|
||||||
|
drop(dbs); // Release the read lock
|
||||||
|
|
||||||
|
// Create a new instance
|
||||||
|
let db = Arc::new(RwLock::new(ACLDB::new(circle_id)?));
|
||||||
|
|
||||||
|
// Store it in the map
|
||||||
|
let mut dbs = self.dbs.write().await;
|
||||||
|
dbs.insert(circle_id.to_string(), Arc::clone(&db));
|
||||||
|
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server for handling RPC requests
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Server {
|
||||||
|
/// Server configuration
|
||||||
|
config: ServerConfig,
|
||||||
|
/// RPC interface
|
||||||
|
rpc: Arc<RpcInterface>,
|
||||||
|
/// Map of circle IDs to request queues
|
||||||
|
queues: RwLock<HashMap<String, CircleQueue>>,
|
||||||
|
/// Factory for creating ACLDB instances
|
||||||
|
acldb_factory: Arc<ACLDBFactory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
/// Creates a new server
|
||||||
|
pub fn new(config: ServerConfig) -> Self {
|
||||||
|
let rpc = Arc::new(RpcInterface::new());
|
||||||
|
let queues = RwLock::new(HashMap::new());
|
||||||
|
let acldb_factory = Arc::new(ACLDBFactory::new());
|
||||||
|
|
||||||
|
Server {
|
||||||
|
config,
|
||||||
|
rpc,
|
||||||
|
queues,
|
||||||
|
acldb_factory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the server
|
||||||
|
pub async fn start(&self) -> std::io::Result<()> {
|
||||||
|
let server_data = web::Data::new(self.clone());
|
||||||
|
|
||||||
|
// Start the HTTP server
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(Logger::default())
|
||||||
|
.wrap(
|
||||||
|
Cors::default()
|
||||||
|
.allow_any_origin()
|
||||||
|
.allow_any_method()
|
||||||
|
.allow_any_header()
|
||||||
|
.max_age(3600)
|
||||||
|
)
|
||||||
|
.app_data(web::Data::clone(&server_data))
|
||||||
|
.route("/rpc", web::post().to(handle_rpc))
|
||||||
|
.route("/health", web::get().to(health_check))
|
||||||
|
.service(
|
||||||
|
SwaggerUi::new("/swagger-ui/{_:.*}")
|
||||||
|
.url("/api-docs/openapi.json", ApiDoc::openapi())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind(format!("{}:{}", self.config.host, self.config.port))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers RPC handlers
|
||||||
|
fn register_handlers(&self) {
|
||||||
|
// Nothing to do here - handlers are now processed dynamically
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a request to the queue for a circle
|
||||||
|
async fn add_to_queue(&self, circle_id: &str, request: RpcRequest) -> mpsc::Receiver<RpcResponse> {
|
||||||
|
let (response_sender, response_receiver) = mpsc::channel(1);
|
||||||
|
|
||||||
|
// Get or create the queue for this circle
|
||||||
|
let mut queues = self.queues.write().await;
|
||||||
|
|
||||||
|
if !queues.contains_key(circle_id) {
|
||||||
|
queues.insert(circle_id.to_string(), CircleQueue::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a mutable reference to the queue
|
||||||
|
if let Some(queue) = queues.get_mut(circle_id) {
|
||||||
|
// Add the request to the queue
|
||||||
|
queue.add_request(
|
||||||
|
request,
|
||||||
|
response_sender,
|
||||||
|
Arc::clone(&self.rpc),
|
||||||
|
Arc::clone(&self.acldb_factory)
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
response_receiver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Extracts the circle ID from an RPC request
|
||||||
|
fn extract_circle_id(request: &web::Json<RpcRequest>) -> Result<String, Error> {
|
||||||
|
// Extract from different parameter types based on the method
|
||||||
|
match request.method.as_str() {
|
||||||
|
"aclupdate" => {
|
||||||
|
let params: AclUpdateParams = serde_json::from_value(request.params.clone())?;
|
||||||
|
Ok(params.circle_id)
|
||||||
|
}
|
||||||
|
"aclremove" => {
|
||||||
|
let params: AclRemoveParams = serde_json::from_value(request.params.clone())?;
|
||||||
|
Ok(params.circle_id)
|
||||||
|
}
|
||||||
|
"acldel" => {
|
||||||
|
let params: AclDelParams = serde_json::from_value(request.params.clone())?;
|
||||||
|
Ok(params.circle_id)
|
||||||
|
}
|
||||||
|
"set" => {
|
||||||
|
let params: SetParams = serde_json::from_value(request.params.clone())?;
|
||||||
|
Ok(params.circle_id)
|
||||||
|
}
|
||||||
|
"del" => {
|
||||||
|
let params: DelParams = serde_json::from_value(request.params.clone())?;
|
||||||
|
Ok(params.circle_id)
|
||||||
|
}
|
||||||
|
"get" => {
|
||||||
|
let params: GetParams = serde_json::from_value(request.params.clone())?;
|
||||||
|
Ok(params.circle_id)
|
||||||
|
}
|
||||||
|
"prefix" => {
|
||||||
|
let params: PrefixParams = serde_json::from_value(request.params.clone())?;
|
||||||
|
Ok(params.circle_id)
|
||||||
|
}
|
||||||
|
_ => Err(Error::InvalidRequest(format!("Unknown method: {}", request.method))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API documentation schema
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
health_check,
|
||||||
|
handle_rpc
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(RpcRequest, RpcResponse)
|
||||||
|
),
|
||||||
|
tags(
|
||||||
|
(name = "acldb", description = "ACLDB API")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
struct ApiDoc;
|
||||||
|
|
||||||
|
/// Handler for RPC requests with OpenAPI documentation
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/rpc",
|
||||||
|
request_body = RpcRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "RPC request processed successfully", body = RpcResponse),
|
||||||
|
(status = 400, description = "Bad request", body = RpcResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = RpcResponse)
|
||||||
|
),
|
||||||
|
tag = "acldb"
|
||||||
|
)]
|
||||||
|
async fn handle_rpc(
|
||||||
|
server: web::Data<Server>,
|
||||||
|
request: web::Json<RpcRequest>,
|
||||||
|
) -> impl Responder {
|
||||||
|
// Extract the circle ID from the request
|
||||||
|
let circle_id = match extract_circle_id(&request) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(err) => {
|
||||||
|
return HttpResponse::BadRequest().json(RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the request to the queue for this circle
|
||||||
|
let mut response_receiver = server.add_to_queue(&circle_id, request.0.clone()).await;
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
match response_receiver.recv().await {
|
||||||
|
Some(response) => HttpResponse::Ok().json(response),
|
||||||
|
None => HttpResponse::InternalServerError().json(RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some("Failed to get response".to_string()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process an RPC request
|
||||||
|
async fn process_request(
|
||||||
|
request: &RpcRequest,
|
||||||
|
rpc_interface: &Arc<RpcInterface>,
|
||||||
|
acldb_factory: &Arc<ACLDBFactory>
|
||||||
|
) -> RpcResponse {
|
||||||
|
match request.method.as_str() {
|
||||||
|
"aclupdate" => {
|
||||||
|
match serde_json::from_value::<AclUpdateParams>(request.params.clone()) {
|
||||||
|
Ok(params) => {
|
||||||
|
match RpcInterface::parse_acl_right(¶ms.right) {
|
||||||
|
Ok(right) => {
|
||||||
|
match acldb_factory.get_or_create(¶ms.circle_id).await {
|
||||||
|
Ok(db) => {
|
||||||
|
let mut db = db.write().await;
|
||||||
|
match db.acl_update(¶ms.caller_pubkey, ¶ms.name, ¶ms.pubkeys, right) {
|
||||||
|
Ok(_) => RpcResponse {
|
||||||
|
result: Some(json!({"success": true})),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid parameters: {}", err)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aclremove" => {
|
||||||
|
match serde_json::from_value::<AclRemoveParams>(request.params.clone()) {
|
||||||
|
Ok(params) => {
|
||||||
|
match acldb_factory.get_or_create(¶ms.circle_id).await {
|
||||||
|
Ok(db) => {
|
||||||
|
let mut db = db.write().await;
|
||||||
|
match db.acl_remove(¶ms.caller_pubkey, ¶ms.name, ¶ms.pubkeys) {
|
||||||
|
Ok(_) => RpcResponse {
|
||||||
|
result: Some(json!({"success": true})),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid parameters: {}", err)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acldel" => {
|
||||||
|
match serde_json::from_value::<AclDelParams>(request.params.clone()) {
|
||||||
|
Ok(params) => {
|
||||||
|
match acldb_factory.get_or_create(¶ms.circle_id).await {
|
||||||
|
Ok(db) => {
|
||||||
|
let mut db = db.write().await;
|
||||||
|
match db.acl_del(¶ms.caller_pubkey, ¶ms.name) {
|
||||||
|
Ok(_) => RpcResponse {
|
||||||
|
result: Some(json!({"success": true})),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid parameters: {}", err)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"set" => {
|
||||||
|
match serde_json::from_value::<SetParams>(request.params.clone()) {
|
||||||
|
Ok(params) => {
|
||||||
|
match acldb_factory.get_or_create(¶ms.circle_id).await {
|
||||||
|
Ok(db) => {
|
||||||
|
let mut db = db.write().await;
|
||||||
|
let topic = db.topic(¶ms.topic);
|
||||||
|
|
||||||
|
match base64_decode(¶ms.value) {
|
||||||
|
Ok(value) => {
|
||||||
|
let acl_id = params.acl_id.unwrap_or(0);
|
||||||
|
|
||||||
|
let result = if let Some(key) = params.key {
|
||||||
|
let topic = topic.write().await;
|
||||||
|
topic.set_with_acl(&key, &value, acl_id)
|
||||||
|
} else if let Some(id) = params.id {
|
||||||
|
let topic = topic.write().await;
|
||||||
|
topic.set_with_acl(&id.to_string(), &value, acl_id)
|
||||||
|
} else {
|
||||||
|
Err(Error::InvalidRequest("Either key or id must be provided".to_string()))
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(id) => RpcResponse {
|
||||||
|
result: Some(json!({"id": id})),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid parameters: {}", err)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => RpcResponse {
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Unknown method: {}", request.method)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler for health check with OpenAPI documentation
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/health",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Server is healthy", body = String)
|
||||||
|
),
|
||||||
|
tag = "acldb"
|
||||||
|
)]
|
||||||
|
async fn health_check() -> impl Responder {
|
||||||
|
HttpResponse::Ok().json(json!({"status": "ok"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
// Tests will be added here
|
||||||
|
}
|
344
acldb/src/topic.rs
Normal file
344
acldb/src/topic.rs
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use ourdb::{OurDB, OurDBSetArgs};
|
||||||
|
use tst::TST;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::acl::{ACL, ACLRight};
|
||||||
|
|
||||||
|
/// ACLDBTopic represents a database instance for a specific topic within a circle
|
||||||
|
pub struct ACLDBTopic {
|
||||||
|
/// Circle ID
|
||||||
|
circle_id: String,
|
||||||
|
/// Topic name
|
||||||
|
topic: String,
|
||||||
|
/// OurDB instance
|
||||||
|
db: Arc<RwLock<OurDB>>,
|
||||||
|
/// TST instance for key-to-id mapping
|
||||||
|
tst: Arc<RwLock<TST>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ACLDBTopic {
|
||||||
|
/// Creates a new ACLDBTopic instance
|
||||||
|
pub fn new(circle_id: String, topic: String, db: Arc<RwLock<OurDB>>, tst: Arc<RwLock<TST>>) -> Self {
|
||||||
|
ACLDBTopic {
|
||||||
|
circle_id,
|
||||||
|
topic,
|
||||||
|
db,
|
||||||
|
tst,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a value in the database with optional ACL protection
|
||||||
|
pub async fn set(&self, key: &str, value: &[u8]) -> Result<u32, Error> {
|
||||||
|
self.set_with_acl(key, value, 0).await // 0 means no ACL protection
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a value in the database with ACL protection
|
||||||
|
pub async fn set_with_acl(&self, key: &str, value: &[u8], acl_id: u32) -> Result<u32, Error> {
|
||||||
|
// Create the TST key
|
||||||
|
let tst_key = format!("{}{}", self.topic, key);
|
||||||
|
|
||||||
|
// Check if the key already exists in TST
|
||||||
|
let id = {
|
||||||
|
// First try to get the ID from TST
|
||||||
|
let mut id_opt = None;
|
||||||
|
{
|
||||||
|
let tst = self.tst.read().await;
|
||||||
|
if let Ok(id_bytes) = tst.list(&tst_key) {
|
||||||
|
if !id_bytes.is_empty() {
|
||||||
|
let id_str = String::from_utf8_lossy(&id_bytes[0].as_bytes());
|
||||||
|
if let Ok(parsed_id) = id_str.parse::<u32>() {
|
||||||
|
id_opt = Some(parsed_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, get a new ID
|
||||||
|
match id_opt {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.get_next_id()?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare the data with ACL ID prefix if needed
|
||||||
|
let data = if acl_id > 0 {
|
||||||
|
// Add ACL ID as the first 4 bytes
|
||||||
|
let mut acl_data = acl_id.to_be_bytes().to_vec();
|
||||||
|
acl_data.extend_from_slice(value);
|
||||||
|
acl_data
|
||||||
|
} else {
|
||||||
|
value.to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store the data in OurDB
|
||||||
|
{
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.set(OurDBSetArgs {
|
||||||
|
id: Some(id),
|
||||||
|
data: &data,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the ID in TST
|
||||||
|
{
|
||||||
|
let mut tst = self.tst.write().await;
|
||||||
|
tst.set(&tst_key, id.to_string().into_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a value from the database
|
||||||
|
pub async fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
|
||||||
|
// Create the TST key
|
||||||
|
let tst_key = format!("{}{}", self.topic, key);
|
||||||
|
|
||||||
|
// Get the ID from TST
|
||||||
|
let id = {
|
||||||
|
let tst = self.tst.read().await;
|
||||||
|
let keys = tst.list(&tst_key)?;
|
||||||
|
if keys.is_empty() {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
let id_str = &keys[0];
|
||||||
|
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the data from OurDB
|
||||||
|
let data = {
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.get(id)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the data has an ACL ID prefix
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let (acl_id_bytes, actual_data) = data.split_at(4);
|
||||||
|
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
|
||||||
|
|
||||||
|
if acl_id > 0 {
|
||||||
|
// This record is ACL-protected, but we're not checking permissions here
|
||||||
|
// The permission check should be done at a higher level
|
||||||
|
return Ok(actual_data.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a value from the database with permission check
|
||||||
|
pub async fn get_with_permission(&self, key: &str, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<Vec<u8>, Error> {
|
||||||
|
// Create the TST key
|
||||||
|
let tst_key = format!("{}{}", self.topic, key);
|
||||||
|
|
||||||
|
// Get the ID from TST
|
||||||
|
let id = {
|
||||||
|
let tst = self.tst.read().await;
|
||||||
|
let keys = tst.list(&tst_key)?;
|
||||||
|
if keys.is_empty() {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
let id_str = &keys[0];
|
||||||
|
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the data from OurDB
|
||||||
|
let data = {
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.get(id)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the data has an ACL ID prefix
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let (acl_id_bytes, actual_data) = data.split_at(4);
|
||||||
|
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
|
||||||
|
|
||||||
|
if acl_id > 0 {
|
||||||
|
// This record is ACL-protected, check permissions
|
||||||
|
let acl_name = format!("acl_{}", acl_id);
|
||||||
|
|
||||||
|
// Find the ACL in the parent ACLs
|
||||||
|
let has_permission = parent_acls.iter()
|
||||||
|
.find(|acl| acl.name == acl_name)
|
||||||
|
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Read))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !has_permission {
|
||||||
|
return Err(Error::PermissionDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(actual_data.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a value by ID from the database
|
||||||
|
pub async fn get_by_id(&self, id: u32) -> Result<Vec<u8>, Error> {
|
||||||
|
// Get the data from OurDB
|
||||||
|
let data = {
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.get(id)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the data has an ACL ID prefix
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let (_, actual_data) = data.split_at(4);
|
||||||
|
return Ok(actual_data.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a value by ID from the database with permission check
|
||||||
|
pub async fn get_by_id_with_permission(&self, id: u32, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<Vec<u8>, Error> {
|
||||||
|
// Get the data from OurDB
|
||||||
|
let data = {
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.get(id)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the data has an ACL ID prefix
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let (acl_id_bytes, actual_data) = data.split_at(4);
|
||||||
|
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
|
||||||
|
|
||||||
|
if acl_id > 0 {
|
||||||
|
// This record is ACL-protected, check permissions
|
||||||
|
let acl_name = format!("acl_{}", acl_id);
|
||||||
|
|
||||||
|
// Find the ACL in the parent ACLs
|
||||||
|
let has_permission = parent_acls.iter()
|
||||||
|
.find(|acl| acl.name == acl_name)
|
||||||
|
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Read))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !has_permission {
|
||||||
|
return Err(Error::PermissionDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(actual_data.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a value from the database
|
||||||
|
pub async fn delete(&self, key: &str) -> Result<(), Error> {
|
||||||
|
// Create the TST key
|
||||||
|
let tst_key = format!("{}{}", self.topic, key);
|
||||||
|
|
||||||
|
// Get the ID from TST
|
||||||
|
let id = {
|
||||||
|
let tst = self.tst.read().await;
|
||||||
|
let keys = tst.list(&tst_key)?;
|
||||||
|
if keys.is_empty() {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
let id_str = &keys[0];
|
||||||
|
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete from OurDB
|
||||||
|
{
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.delete(id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete from TST
|
||||||
|
{
|
||||||
|
let mut tst = self.tst.write().await;
|
||||||
|
tst.delete(&tst_key)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a value from the database with permission check
|
||||||
|
pub async fn delete_with_permission(&self, key: &str, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<(), Error> {
|
||||||
|
// Create the TST key
|
||||||
|
let tst_key = format!("{}{}", self.topic, key);
|
||||||
|
|
||||||
|
// Get the ID from TST
|
||||||
|
let id = {
|
||||||
|
let tst = self.tst.read().await;
|
||||||
|
let keys = tst.list(&tst_key)?;
|
||||||
|
if keys.is_empty() {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
let id_str = &keys[0];
|
||||||
|
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the data to check ACL
|
||||||
|
let data = {
|
||||||
|
let db = self.db.read().await;
|
||||||
|
db.get(id)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the data has an ACL ID prefix
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let (acl_id_bytes, _) = data.split_at(4);
|
||||||
|
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
|
||||||
|
|
||||||
|
if acl_id > 0 {
|
||||||
|
// This record is ACL-protected, check permissions
|
||||||
|
let acl_name = format!("acl_{}", acl_id);
|
||||||
|
|
||||||
|
// Find the ACL in the parent ACLs
|
||||||
|
let has_permission = parent_acls.iter()
|
||||||
|
.find(|acl| acl.name == acl_name)
|
||||||
|
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Delete))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !has_permission {
|
||||||
|
return Err(Error::PermissionDenied);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete from OurDB
|
||||||
|
{
|
||||||
|
let mut db = self.db.write().await;
|
||||||
|
db.delete(id)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete from TST
|
||||||
|
{
|
||||||
|
let mut tst = self.tst.write().await;
|
||||||
|
tst.delete(&tst_key)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all keys with a given prefix
|
||||||
|
pub async fn prefix(&self, prefix: &str) -> Result<Vec<String>, Error> {
|
||||||
|
// Create the TST prefix
|
||||||
|
let tst_prefix = format!("{}{}", self.topic, prefix);
|
||||||
|
|
||||||
|
// Get all keys with the prefix
|
||||||
|
let keys = {
|
||||||
|
let tst = self.tst.read().await;
|
||||||
|
tst.list(&tst_prefix)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the topic prefix from the keys
|
||||||
|
let topic_prefix = format!("{}", self.topic);
|
||||||
|
let keys = keys.into_iter()
|
||||||
|
.map(|key| key.strip_prefix(&topic_prefix).unwrap_or(&key).to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
// Tests will be added here
|
||||||
|
}
|
52
acldb/src/utils.rs
Normal file
52
acldb/src/utils.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
/// Decodes a base64 string to bytes
|
||||||
|
pub fn base64_decode(input: &str) -> Result<Vec<u8>, Error> {
|
||||||
|
base64::decode(input).map_err(|e| Error::InvalidRequest(format!("Invalid base64: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes bytes to a base64 string
|
||||||
|
pub fn base64_encode(input: &[u8]) -> String {
|
||||||
|
base64::encode(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a SHA-256 hash of the input
|
||||||
|
pub fn sha256_hash(input: &[u8]) -> Vec<u8> {
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(input);
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a hash to a hex string
|
||||||
|
pub fn to_hex(bytes: &[u8]) -> String {
|
||||||
|
hex::encode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a signature against a message and public key
|
||||||
|
pub fn validate_signature(message: &[u8], signature: &str, pubkey: &str) -> Result<bool, Error> {
|
||||||
|
// In a real implementation, this would validate the cryptographic signature
|
||||||
|
// For now, we'll just return true
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_base64() {
|
||||||
|
let original = b"Hello, world!";
|
||||||
|
let encoded = base64_encode(original);
|
||||||
|
let decoded = base64_decode(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded, original);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sha256() {
|
||||||
|
let input = b"test";
|
||||||
|
let hash = sha256_hash(input);
|
||||||
|
let hex = to_hex(&hash);
|
||||||
|
assert_eq!(hex, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
|
||||||
|
}
|
||||||
|
}
|
304
acldb/static/openapi.json
Normal file
304
acldb/static/openapi.json
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "ACLDB API",
|
||||||
|
"description": "API for the ACLDB module which implements an Access Control List layer on top of the existing ourdb and tst databases.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8080",
|
||||||
|
"description": "Local development server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/rpc": {
|
||||||
|
"post": {
|
||||||
|
"summary": "RPC endpoint",
|
||||||
|
"description": "Handles all RPC requests to the ACLDB system",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RpcRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RpcResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Health check",
|
||||||
|
"description": "Returns the health status of the server",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Server is healthy",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"RpcRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["method", "params", "signature"],
|
||||||
|
"properties": {
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the method to call",
|
||||||
|
"example": "set"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The parameters for the method"
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Cryptographic signature of the request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RpcResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"result": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The result of the method call if successful"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Error message if the method call failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Error message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AclUpdateParams": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["caller_pubkey", "circle_id", "name", "pubkeys", "right"],
|
||||||
|
"properties": {
|
||||||
|
"caller_pubkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Public key of the requesting user"
|
||||||
|
},
|
||||||
|
"circle_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the circle where the ACL exists"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique name for the ACL within the circle"
|
||||||
|
},
|
||||||
|
"pubkeys": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of public keys to grant permissions to"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Permission level (read/write/delete/execute/admin)",
|
||||||
|
"enum": ["read", "write", "delete", "execute", "admin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AclRemoveParams": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["caller_pubkey", "circle_id", "name", "pubkeys"],
|
||||||
|
"properties": {
|
||||||
|
"caller_pubkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Public key of the requesting user"
|
||||||
|
},
|
||||||
|
"circle_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the circle where the ACL exists"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the ACL to modify"
|
||||||
|
},
|
||||||
|
"pubkeys": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of public keys to remove from the ACL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AclDelParams": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["caller_pubkey", "circle_id", "name"],
|
||||||
|
"properties": {
|
||||||
|
"caller_pubkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Public key of the requesting user"
|
||||||
|
},
|
||||||
|
"circle_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the circle where the ACL exists"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the ACL to delete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SetParams": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["caller_pubkey", "circle_id", "topic", "value"],
|
||||||
|
"properties": {
|
||||||
|
"caller_pubkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Public key of the requesting user"
|
||||||
|
},
|
||||||
|
"circle_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the circle where the data belongs"
|
||||||
|
},
|
||||||
|
"topic": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "String identifier for the database category"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional string key for the record"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Optional numeric ID for direct access"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Base64-encoded data to store"
|
||||||
|
},
|
||||||
|
"acl_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "ID of the ACL to protect this record (0 for public access)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DelParams": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["caller_pubkey", "circle_id", "topic"],
|
||||||
|
"properties": {
|
||||||
|
"caller_pubkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Public key of the requesting user"
|
||||||
|
},
|
||||||
|
"circle_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the circle where the data belongs"
|
||||||
|
},
|
||||||
|
"topic": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "String identifier for the database category"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional string key for the record"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Optional numeric ID for direct access"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GetParams": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["caller_pubkey", "circle_id", "topic"],
|
||||||
|
"properties": {
|
||||||
|
"caller_pubkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Public key of the requesting user"
|
||||||
|
},
|
||||||
|
"circle_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the circle where the data belongs"
|
||||||
|
},
|
||||||
|
"topic": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "String identifier for the database category"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional string key for the record"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Optional numeric ID for direct access"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PrefixParams": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["caller_pubkey", "circle_id", "topic", "prefix"],
|
||||||
|
"properties": {
|
||||||
|
"caller_pubkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Public key of the requesting user"
|
||||||
|
},
|
||||||
|
"circle_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of the circle where the data belongs"
|
||||||
|
},
|
||||||
|
"topic": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "String identifier for the database category"
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Prefix to search for"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
acldb/static/swagger-ui.html
Normal file
54
acldb/static/swagger-ui.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ACLDB API Documentation</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" />
|
||||||
|
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.5.0/favicon-32x32.png" sizes="32x32" />
|
||||||
|
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.5.0/favicon-16x16.png" sizes="16x16" />
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: -moz-scrollbars-vertical;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js"></script>
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: "/openapi.json",
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout"
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ui = ui;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
108
herodb/examples/circle_basic_demo.rs
Normal file
108
herodb/examples/circle_basic_demo.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// This example demonstrates the basic functionality of the circle models
|
||||||
|
// without using the database functionality
|
||||||
|
|
||||||
|
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Circle Module Basic Demo");
|
||||||
|
|
||||||
|
// Create a circle
|
||||||
|
let circle = Circle::new(
|
||||||
|
1,
|
||||||
|
"ThreeFold Community".to_string(),
|
||||||
|
"A circle for ThreeFold community members".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Created circle: {:?}", circle);
|
||||||
|
|
||||||
|
// Create members
|
||||||
|
let mut alice = Member::new(
|
||||||
|
1,
|
||||||
|
"Alice".to_string(),
|
||||||
|
"Core contributor".to_string(),
|
||||||
|
Role::Admin,
|
||||||
|
);
|
||||||
|
alice.add_email("alice@example.com".to_string());
|
||||||
|
|
||||||
|
let mut bob = Member::new(
|
||||||
|
2,
|
||||||
|
"Bob".to_string(),
|
||||||
|
"Community member".to_string(),
|
||||||
|
Role::Member,
|
||||||
|
);
|
||||||
|
bob.add_email("bob@example.com".to_string());
|
||||||
|
|
||||||
|
println!("Created members: {:?}, {:?}", alice, bob);
|
||||||
|
|
||||||
|
// Create a domain name
|
||||||
|
let mut domain = Name::new(
|
||||||
|
1,
|
||||||
|
"threefold.io".to_string(),
|
||||||
|
"ThreeFold main domain".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let record = Record {
|
||||||
|
name: "www".to_string(),
|
||||||
|
text: "ThreeFold Website".to_string(),
|
||||||
|
category: RecordType::A,
|
||||||
|
addr: vec!["192.168.1.1".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.add_record(record);
|
||||||
|
domain.add_admin("alice_pubkey".to_string());
|
||||||
|
|
||||||
|
println!("Created domain: {:?}", domain);
|
||||||
|
|
||||||
|
// Create wallets
|
||||||
|
let mut alice_wallet = Wallet::new(
|
||||||
|
1,
|
||||||
|
"Alice's TFT Wallet".to_string(),
|
||||||
|
"Main TFT wallet".to_string(),
|
||||||
|
"Stellar".to_string(),
|
||||||
|
"GALICE...".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
alice_wallet.set_asset("TFT".to_string(), 1000.0);
|
||||||
|
alice_wallet.set_asset("XLM".to_string(), 100.0);
|
||||||
|
|
||||||
|
let mut bob_wallet = Wallet::new(
|
||||||
|
2,
|
||||||
|
"Bob's TFT Wallet".to_string(),
|
||||||
|
"Main TFT wallet".to_string(),
|
||||||
|
"Stellar".to_string(),
|
||||||
|
"GBOB...".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
bob_wallet.set_asset("TFT".to_string(), 500.0);
|
||||||
|
|
||||||
|
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
|
||||||
|
|
||||||
|
// Link wallets to members
|
||||||
|
alice.link_wallet(alice_wallet.id);
|
||||||
|
bob.link_wallet(bob_wallet.id);
|
||||||
|
|
||||||
|
println!("Linked wallets to members");
|
||||||
|
|
||||||
|
// Demonstrate wallet operations
|
||||||
|
println!("\nDemonstrating wallet operations:");
|
||||||
|
|
||||||
|
println!("Alice's wallet before transfer: {:?}", alice_wallet);
|
||||||
|
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||||
|
|
||||||
|
println!("Bob's wallet before transfer: {:?}", bob_wallet);
|
||||||
|
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||||
|
|
||||||
|
// Simulate a transfer of 100 TFT from Alice to Bob
|
||||||
|
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
|
||||||
|
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
|
||||||
|
|
||||||
|
println!("Alice's wallet after transfer: {:?}", alice_wallet);
|
||||||
|
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||||
|
|
||||||
|
println!("Bob's wallet after transfer: {:?}", bob_wallet);
|
||||||
|
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||||
|
|
||||||
|
println!("\nCircle basic demo completed successfully!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
151
herodb/examples/circle_models_demo.rs
Normal file
151
herodb/examples/circle_models_demo.rs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
use herodb::db::{DB, DBBuilder};
|
||||||
|
use herodb::models::circle::{Circle, Member, Name, Wallet, Asset, Role, Record, RecordType};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Create a temporary directory for the database
|
||||||
|
let db_path = Path::new("./tmp/circle_demo");
|
||||||
|
if db_path.exists() {
|
||||||
|
std::fs::remove_dir_all(db_path)?;
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(db_path)?;
|
||||||
|
|
||||||
|
println!("Creating database at {:?}", db_path);
|
||||||
|
|
||||||
|
// Create a database with all circle models registered
|
||||||
|
let db = DBBuilder::new(db_path)
|
||||||
|
.register_model::<Circle>()
|
||||||
|
.register_model::<Member>()
|
||||||
|
.register_model::<Name>()
|
||||||
|
.register_model::<Wallet>()
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
// Create a circle
|
||||||
|
let mut circle = Circle::new(
|
||||||
|
1,
|
||||||
|
"ThreeFold Community".to_string(),
|
||||||
|
"A circle for ThreeFold community members".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Created circle: {:?}", circle);
|
||||||
|
db.set(&circle)?;
|
||||||
|
|
||||||
|
// Create members
|
||||||
|
let mut alice = Member::new(
|
||||||
|
1,
|
||||||
|
"Alice".to_string(),
|
||||||
|
"Core contributor".to_string(),
|
||||||
|
Role::Admin,
|
||||||
|
);
|
||||||
|
alice.add_email("alice@example.com".to_string());
|
||||||
|
|
||||||
|
let mut bob = Member::new(
|
||||||
|
2,
|
||||||
|
"Bob".to_string(),
|
||||||
|
"Community member".to_string(),
|
||||||
|
Role::Member,
|
||||||
|
);
|
||||||
|
bob.add_email("bob@example.com".to_string());
|
||||||
|
|
||||||
|
println!("Created members: {:?}, {:?}", alice, bob);
|
||||||
|
db.set(&alice)?;
|
||||||
|
db.set(&bob)?;
|
||||||
|
|
||||||
|
// Create a domain name
|
||||||
|
let mut domain = Name::new(
|
||||||
|
1,
|
||||||
|
"threefold.io".to_string(),
|
||||||
|
"ThreeFold main domain".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let record = Record {
|
||||||
|
name: "www".to_string(),
|
||||||
|
text: "ThreeFold Website".to_string(),
|
||||||
|
category: RecordType::A,
|
||||||
|
addr: vec!["192.168.1.1".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.add_record(record);
|
||||||
|
domain.add_admin("alice_pubkey".to_string());
|
||||||
|
|
||||||
|
println!("Created domain: {:?}", domain);
|
||||||
|
db.set(&domain)?;
|
||||||
|
|
||||||
|
// Create wallets
|
||||||
|
let mut alice_wallet = Wallet::new(
|
||||||
|
1,
|
||||||
|
"Alice's TFT Wallet".to_string(),
|
||||||
|
"Main TFT wallet".to_string(),
|
||||||
|
"Stellar".to_string(),
|
||||||
|
"GALICE...".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
alice_wallet.set_asset("TFT".to_string(), 1000.0);
|
||||||
|
alice_wallet.set_asset("XLM".to_string(), 100.0);
|
||||||
|
|
||||||
|
let mut bob_wallet = Wallet::new(
|
||||||
|
2,
|
||||||
|
"Bob's TFT Wallet".to_string(),
|
||||||
|
"Main TFT wallet".to_string(),
|
||||||
|
"Stellar".to_string(),
|
||||||
|
"GBOB...".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
bob_wallet.set_asset("TFT".to_string(), 500.0);
|
||||||
|
|
||||||
|
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
|
||||||
|
db.set(&alice_wallet)?;
|
||||||
|
db.set(&bob_wallet)?;
|
||||||
|
|
||||||
|
// Link wallets to members
|
||||||
|
alice.link_wallet(alice_wallet.id);
|
||||||
|
bob.link_wallet(bob_wallet.id);
|
||||||
|
|
||||||
|
db.set(&alice)?;
|
||||||
|
db.set(&bob)?;
|
||||||
|
|
||||||
|
// Retrieve and display all data
|
||||||
|
println!("\nRetrieving data from database:");
|
||||||
|
|
||||||
|
let circles = db.list_circles()?;
|
||||||
|
println!("Circles: {:#?}", circles);
|
||||||
|
|
||||||
|
let members = db.list_members()?;
|
||||||
|
println!("Members: {:#?}", members);
|
||||||
|
|
||||||
|
let names = db.list_names()?;
|
||||||
|
println!("Names: {:#?}", names);
|
||||||
|
|
||||||
|
let wallets = db.list_wallets()?;
|
||||||
|
println!("Wallets: {:#?}", wallets);
|
||||||
|
|
||||||
|
// Demonstrate wallet operations
|
||||||
|
println!("\nDemonstrating wallet operations:");
|
||||||
|
|
||||||
|
let mut alice_wallet = db.get_wallet(1)?;
|
||||||
|
println!("Alice's wallet before transfer: {:?}", alice_wallet);
|
||||||
|
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||||
|
|
||||||
|
let mut bob_wallet = db.get_wallet(2)?;
|
||||||
|
println!("Bob's wallet before transfer: {:?}", bob_wallet);
|
||||||
|
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||||
|
|
||||||
|
// Simulate a transfer of 100 TFT from Alice to Bob
|
||||||
|
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
|
||||||
|
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
|
||||||
|
|
||||||
|
db.set(&alice_wallet)?;
|
||||||
|
db.set(&bob_wallet)?;
|
||||||
|
|
||||||
|
let alice_wallet = db.get_wallet(1)?;
|
||||||
|
println!("Alice's wallet after transfer: {:?}", alice_wallet);
|
||||||
|
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||||
|
|
||||||
|
let bob_wallet = db.get_wallet(2)?;
|
||||||
|
println!("Bob's wallet after transfer: {:?}", bob_wallet);
|
||||||
|
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||||
|
|
||||||
|
println!("\nCircle models demo completed successfully!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
106
herodb/examples/circle_standalone.rs
Normal file
106
herodb/examples/circle_standalone.rs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
//! This is a standalone example that demonstrates the circle models
|
||||||
|
//! without using any database functionality.
|
||||||
|
|
||||||
|
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Circle Module Standalone Demo");
|
||||||
|
|
||||||
|
// Create a circle
|
||||||
|
let circle = Circle::new(
|
||||||
|
1,
|
||||||
|
"ThreeFold Community".to_string(),
|
||||||
|
"A circle for ThreeFold community members".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Created circle: {:?}", circle);
|
||||||
|
|
||||||
|
// Create members
|
||||||
|
let mut alice = Member::new(
|
||||||
|
1,
|
||||||
|
"Alice".to_string(),
|
||||||
|
"Core contributor".to_string(),
|
||||||
|
Role::Admin,
|
||||||
|
);
|
||||||
|
alice.add_email("alice@example.com".to_string());
|
||||||
|
|
||||||
|
let mut bob = Member::new(
|
||||||
|
2,
|
||||||
|
"Bob".to_string(),
|
||||||
|
"Community member".to_string(),
|
||||||
|
Role::Member,
|
||||||
|
);
|
||||||
|
bob.add_email("bob@example.com".to_string());
|
||||||
|
|
||||||
|
println!("Created members: {:?}, {:?}", alice, bob);
|
||||||
|
|
||||||
|
// Create a domain name
|
||||||
|
let mut domain = Name::new(
|
||||||
|
1,
|
||||||
|
"threefold.io".to_string(),
|
||||||
|
"ThreeFold main domain".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let record = Record {
|
||||||
|
name: "www".to_string(),
|
||||||
|
text: "ThreeFold Website".to_string(),
|
||||||
|
category: RecordType::A,
|
||||||
|
addr: vec!["192.168.1.1".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.add_record(record);
|
||||||
|
domain.add_admin("alice_pubkey".to_string());
|
||||||
|
|
||||||
|
println!("Created domain: {:?}", domain);
|
||||||
|
|
||||||
|
// Create wallets
|
||||||
|
let mut alice_wallet = Wallet::new(
|
||||||
|
1,
|
||||||
|
"Alice's TFT Wallet".to_string(),
|
||||||
|
"Main TFT wallet".to_string(),
|
||||||
|
"Stellar".to_string(),
|
||||||
|
"GALICE...".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
alice_wallet.set_asset("TFT".to_string(), 1000.0);
|
||||||
|
alice_wallet.set_asset("XLM".to_string(), 100.0);
|
||||||
|
|
||||||
|
let mut bob_wallet = Wallet::new(
|
||||||
|
2,
|
||||||
|
"Bob's TFT Wallet".to_string(),
|
||||||
|
"Main TFT wallet".to_string(),
|
||||||
|
"Stellar".to_string(),
|
||||||
|
"GBOB...".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
bob_wallet.set_asset("TFT".to_string(), 500.0);
|
||||||
|
|
||||||
|
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
|
||||||
|
|
||||||
|
// Link wallets to members
|
||||||
|
alice.link_wallet(alice_wallet.id);
|
||||||
|
bob.link_wallet(bob_wallet.id);
|
||||||
|
|
||||||
|
println!("Linked wallets to members");
|
||||||
|
|
||||||
|
// Demonstrate wallet operations
|
||||||
|
println!("\nDemonstrating wallet operations:");
|
||||||
|
|
||||||
|
println!("Alice's wallet before transfer: {:?}", alice_wallet);
|
||||||
|
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||||
|
|
||||||
|
println!("Bob's wallet before transfer: {:?}", bob_wallet);
|
||||||
|
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||||
|
|
||||||
|
// Simulate a transfer of 100 TFT from Alice to Bob
|
||||||
|
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
|
||||||
|
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
|
||||||
|
|
||||||
|
println!("Alice's wallet after transfer: {:?}", alice_wallet);
|
||||||
|
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||||
|
|
||||||
|
println!("Bob's wallet after transfer: {:?}", bob_wallet);
|
||||||
|
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||||
|
|
||||||
|
println!("\nCircle standalone demo completed successfully!");
|
||||||
|
}
|
@ -6,6 +6,7 @@ use crate::models::gov::{
|
|||||||
Company, Shareholder, Meeting, User, Vote, Resolution,
|
Company, Shareholder, Meeting, User, Vote, Resolution,
|
||||||
Committee, ComplianceRequirement, ComplianceDocument, ComplianceAudit
|
Committee, ComplianceRequirement, ComplianceDocument, ComplianceAudit
|
||||||
};
|
};
|
||||||
|
use crate::models::circle::{Circle, Member, Name, Wallet, Asset};
|
||||||
|
|
||||||
// Implement model-specific methods for Product
|
// Implement model-specific methods for Product
|
||||||
impl_model_methods!(Product, product, products);
|
impl_model_methods!(Product, product, products);
|
||||||
@ -59,4 +60,16 @@ impl_model_methods!(ComplianceRequirement, compliance_requirement, compliance_re
|
|||||||
impl_model_methods!(ComplianceDocument, compliance_document, compliance_documents);
|
impl_model_methods!(ComplianceDocument, compliance_document, compliance_documents);
|
||||||
|
|
||||||
// Implement model-specific methods for ComplianceAudit
|
// Implement model-specific methods for ComplianceAudit
|
||||||
impl_model_methods!(ComplianceAudit, compliance_audit, compliance_audits);
|
impl_model_methods!(ComplianceAudit, compliance_audit, compliance_audits);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Circle
|
||||||
|
impl_model_methods!(Circle, circle, circles);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Member
|
||||||
|
impl_model_methods!(Member, member, members);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Name
|
||||||
|
impl_model_methods!(Name, name, names);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Wallet
|
||||||
|
impl_model_methods!(Wallet, wallet, wallets);
|
@ -235,6 +235,10 @@ impl ContractBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Storable for Contract {}
|
||||||
|
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Contract {
|
impl Model for Contract {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::db::model::{Model, Storable};
|
use crate::db::model::Model;
|
||||||
|
use crate::db::{Storable, DbError, DbResult};
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -72,6 +73,9 @@ impl CurrencyBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait
|
||||||
|
impl Storable for Currency {}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Currency {
|
impl Model for Currency {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -133,6 +133,9 @@ impl CustomerBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Storable for Customer {}
|
||||||
|
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Customer {
|
impl Model for Customer {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -91,6 +91,9 @@ impl ExchangeRateBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
impl Storable for ExchangeRate {}
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for ExchangeRate {
|
impl Model for ExchangeRate {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -500,6 +500,8 @@ impl InvoiceBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Storable for Invoice {}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Invoice {
|
impl Model for Invoice {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::db::model::{Model, Storable};
|
use crate::db::model::Model;
|
||||||
|
use crate::db::Storable;
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -342,6 +343,9 @@ impl ProductBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait
|
||||||
|
impl Storable for Product {}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Product {
|
impl Model for Product {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::db::{Model, Storable};
|
use crate::db::{Model, Storable, DbError, DbResult};
|
||||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||||
// use super::db::Model; // Removed old Model trait import
|
// use super::db::Model; // Removed old Model trait import
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@ -566,6 +566,9 @@ impl SaleBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait
|
||||||
|
impl Storable for Sale {}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Sale {
|
impl Model for Sale {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||||
use crate::db::{Model, Storable}; // Import Model trait from db module
|
use crate::db::{Model, Storable, DbError, DbResult}; // Import Model trait from db module
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -491,6 +491,10 @@ impl ServiceBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait
|
||||||
|
impl Storable for Service {
|
||||||
|
}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Service {
|
impl Model for Service {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable};
|
use crate::db::{Model, Storable, DbError, DbResult};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Circle represents a collection of members (users or other circles)
|
/// Circle represents a collection of members (users or other circles)
|
||||||
@ -28,6 +28,9 @@ impl Circle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait
|
||||||
|
impl Storable for Circle {}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Circle {
|
impl Model for Circle {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable};
|
use crate::db::{Model, Storable, DbError, DbResult};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Role represents the role of a member in a circle
|
/// Role represents the role of a member in a circle
|
||||||
@ -67,6 +67,10 @@ impl Member {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait
|
||||||
|
impl Storable for Member {
|
||||||
|
}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Member {
|
impl Model for Member {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable};
|
use crate::db::{Model, Storable, DbError, DbResult};
|
||||||
|
|
||||||
/// Record types for a DNS record
|
/// Record types for a DNS record
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -57,6 +57,10 @@ impl Name {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait
|
||||||
|
impl Storable for Name {
|
||||||
|
}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Name {
|
impl Model for Name {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable};
|
use crate::db::{Model, Storable, DbError, DbResult};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Asset represents a cryptocurrency asset in a wallet
|
/// Asset represents a cryptocurrency asset in a wallet
|
||||||
@ -70,7 +70,8 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implement Storable trait
|
// Implement Storable trait
|
||||||
impl Storable for Wallet {}
|
impl Storable for Wallet {
|
||||||
|
}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Wallet {
|
impl Model for Wallet {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
use crate::db::{Model, Storable, DbError, DbResult};
|
use crate::db::{Model, Storable, DbResult};
|
||||||
|
use crate::db::db::DB;
|
||||||
use super::shareholder::Shareholder; // Use super:: for sibling module
|
use super::shareholder::Shareholder; // Use super:: for sibling module
|
||||||
|
use super::Resolution;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
@ -87,7 +89,7 @@ pub struct Company {
|
|||||||
// Removed shareholders property
|
// Removed shareholders property
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Storable for Company{}
|
||||||
|
|
||||||
// Model requires get_id and db_prefix
|
// Model requires get_id and db_prefix
|
||||||
impl Model for Company {
|
impl Model for Company {
|
||||||
@ -150,24 +152,22 @@ impl Company {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Link this company to a Circle for access control
|
/// Link this company to a Circle for access control
|
||||||
pub fn link_to_circle(&mut self, circle_id: u32) -> DbResult<()> {
|
pub fn link_to_circle(&mut self, circle_id: u32) {
|
||||||
// Implementation would involve updating a mapping in a separate database
|
// Implementation would involve updating a mapping in a separate database
|
||||||
// For now, we'll just update the timestamp to indicate the change
|
// For now, we'll just update the timestamp to indicate the change
|
||||||
self.updated_at = Utc::now();
|
self.updated_at = Utc::now();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Link this company to a Customer in the biz module
|
/// Link this company to a Customer in the biz module
|
||||||
pub fn link_to_customer(&mut self, customer_id: u32) -> DbResult<()> {
|
pub fn link_to_customer(&mut self, customer_id: u32) {
|
||||||
// Implementation would involve updating a mapping in a separate database
|
// Implementation would involve updating a mapping in a separate database
|
||||||
// For now, we'll just update the timestamp to indicate the change
|
// For now, we'll just update the timestamp to indicate the change
|
||||||
self.updated_at = Utc::now();
|
self.updated_at = Utc::now();
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all resolutions for this company
|
/// Get all resolutions for this company
|
||||||
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<super::Resolution>> {
|
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<Resolution>> {
|
||||||
let all_resolutions = db.list::<super::Resolution>()?;
|
let all_resolutions = db.list::<Resolution>()?;
|
||||||
let company_resolutions = all_resolutions
|
let company_resolutions = all_resolutions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|resolution| resolution.company_id == self.id)
|
.filter(|resolution| resolution.company_id == self.id)
|
||||||
|
@ -1,207 +0,0 @@
|
|||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
|
||||||
use crate::models::gov::Company;
|
|
||||||
|
|
||||||
/// ComplianceRequirement represents a regulatory requirement
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct ComplianceRequirement {
|
|
||||||
pub id: u32,
|
|
||||||
pub company_id: u32,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub regulation: String,
|
|
||||||
pub authority: String,
|
|
||||||
pub deadline: DateTime<Utc>,
|
|
||||||
pub status: String,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ComplianceDocument represents a compliance document
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct ComplianceDocument {
|
|
||||||
pub id: u32,
|
|
||||||
pub requirement_id: u32,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub file_path: String,
|
|
||||||
pub file_type: String,
|
|
||||||
pub uploaded_by: u32, // User ID
|
|
||||||
pub uploaded_at: DateTime<Utc>,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ComplianceAudit represents a compliance audit
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct ComplianceAudit {
|
|
||||||
pub id: u32,
|
|
||||||
pub company_id: u32,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub auditor: String,
|
|
||||||
pub start_date: DateTime<Utc>,
|
|
||||||
pub end_date: DateTime<Utc>,
|
|
||||||
pub status: String,
|
|
||||||
pub findings: String,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ComplianceRequirement {
|
|
||||||
/// Create a new compliance requirement with default values
|
|
||||||
pub fn new(
|
|
||||||
id: u32,
|
|
||||||
company_id: u32,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
regulation: String,
|
|
||||||
authority: String,
|
|
||||||
deadline: DateTime<Utc>,
|
|
||||||
) -> Self {
|
|
||||||
let now = Utc::now();
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
company_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
regulation,
|
|
||||||
authority,
|
|
||||||
deadline,
|
|
||||||
status: "Pending".to_string(),
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the status of the requirement
|
|
||||||
pub fn update_status(&mut self, status: String) {
|
|
||||||
self.status = status;
|
|
||||||
self.updated_at = Utc::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the company associated with this requirement
|
|
||||||
pub fn get_company(&self, db: &DB) -> DbResult<Company> {
|
|
||||||
db.get::<Company>(self.company_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all documents associated with this requirement
|
|
||||||
pub fn get_documents(&self, db: &DB) -> DbResult<Vec<ComplianceDocument>> {
|
|
||||||
let all_documents = db.list::<ComplianceDocument>()?;
|
|
||||||
let requirement_documents = all_documents
|
|
||||||
.into_iter()
|
|
||||||
.filter(|doc| doc.requirement_id == self.id)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(requirement_documents)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ComplianceDocument {
|
|
||||||
/// Create a new compliance document with default values
|
|
||||||
pub fn new(
|
|
||||||
id: u32,
|
|
||||||
requirement_id: u32,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
file_path: String,
|
|
||||||
file_type: String,
|
|
||||||
uploaded_by: u32,
|
|
||||||
) -> Self {
|
|
||||||
let now = Utc::now();
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
requirement_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
file_path,
|
|
||||||
file_type,
|
|
||||||
uploaded_by,
|
|
||||||
uploaded_at: now,
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the requirement associated with this document
|
|
||||||
pub fn get_requirement(&self, db: &DB) -> DbResult<ComplianceRequirement> {
|
|
||||||
db.get::<ComplianceRequirement>(self.requirement_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ComplianceAudit {
|
|
||||||
/// Create a new compliance audit with default values
|
|
||||||
pub fn new(
|
|
||||||
id: u32,
|
|
||||||
company_id: u32,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
auditor: String,
|
|
||||||
start_date: DateTime<Utc>,
|
|
||||||
end_date: DateTime<Utc>,
|
|
||||||
) -> Self {
|
|
||||||
let now = Utc::now();
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
company_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
auditor,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
status: "Planned".to_string(),
|
|
||||||
findings: String::new(),
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the status of the audit
|
|
||||||
pub fn update_status(&mut self, status: String) {
|
|
||||||
self.status = status;
|
|
||||||
self.updated_at = Utc::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the findings of the audit
|
|
||||||
pub fn update_findings(&mut self, findings: String) {
|
|
||||||
self.findings = findings;
|
|
||||||
self.updated_at = Utc::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the company associated with this audit
|
|
||||||
pub fn get_company(&self, db: &DB) -> DbResult<Company> {
|
|
||||||
db.get::<Company>(self.company_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement Model trait
|
|
||||||
impl Model for ComplianceRequirement {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"compliance_requirement"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model for ComplianceDocument {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"compliance_document"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model for ComplianceAudit {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"compliance_audit"
|
|
||||||
}
|
|
||||||
}
|
|
@ -175,6 +175,7 @@ impl Meeting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Storable for Meeting{}
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Meeting {
|
impl Model for Meeting {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -6,7 +6,6 @@ pub mod vote;
|
|||||||
pub mod resolution;
|
pub mod resolution;
|
||||||
// All modules:
|
// All modules:
|
||||||
pub mod committee;
|
pub mod committee;
|
||||||
pub mod compliance;
|
|
||||||
|
|
||||||
// Re-export all model types for convenience
|
// Re-export all model types for convenience
|
||||||
pub use company::{Company, CompanyStatus, BusinessType};
|
pub use company::{Company, CompanyStatus, BusinessType};
|
||||||
@ -16,7 +15,6 @@ pub use user::User;
|
|||||||
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
||||||
pub use resolution::{Resolution, ResolutionStatus, Approval};
|
pub use resolution::{Resolution, ResolutionStatus, Approval};
|
||||||
pub use committee::{Committee, CommitteeMember, CommitteeRole};
|
pub use committee::{Committee, CommitteeMember, CommitteeRole};
|
||||||
pub use compliance::{ComplianceRequirement, ComplianceDocument, ComplianceAudit};
|
|
||||||
|
|
||||||
// Re-export database components from db module
|
// Re-export database components from db module
|
||||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult};
|
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult};
|
@ -180,6 +180,9 @@ impl Resolution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Storable for Resolution{}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Resolution {
|
impl Model for Resolution {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -63,6 +63,8 @@ impl Shareholder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Storable for Shareholder{}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for Shareholder {
|
impl Model for Shareholder {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -42,6 +42,8 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Storable for User{}
|
||||||
|
|
||||||
// Implement Model trait
|
// Implement Model trait
|
||||||
impl Model for User {
|
impl Model for User {
|
||||||
fn get_id(&self) -> u32 {
|
fn get_id(&self) -> u32 {
|
||||||
|
@ -51,7 +51,7 @@ pub struct Ballot {
|
|||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed old Model trait implementation
|
impl Storable for Vote{}
|
||||||
|
|
||||||
impl Vote {
|
impl Vote {
|
||||||
/// Create a new vote with default timestamps
|
/// Create a new vote with default timestamps
|
||||||
|
Loading…
Reference in New Issue
Block a user