...
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-rt = "2"
|
||||
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"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
thiserror = "1.0"
|
||||
sha2 = "0.10"
|
||||
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,
|
||||
Committee, ComplianceRequirement, ComplianceDocument, ComplianceAudit
|
||||
};
|
||||
use crate::models::circle::{Circle, Member, Name, Wallet, Asset};
|
||||
|
||||
// Implement model-specific methods for Product
|
||||
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);
|
||||
|
||||
// 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
|
||||
impl Model for Contract {
|
||||
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 rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -72,6 +73,9 @@ impl CurrencyBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Currency {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Currency {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -133,6 +133,9 @@ impl CustomerBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Customer {}
|
||||
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Customer {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -91,6 +91,9 @@ impl ExchangeRateBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Storable for ExchangeRate {}
|
||||
// Implement Model trait
|
||||
impl Model for ExchangeRate {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -500,6 +500,8 @@ impl InvoiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Invoice {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Invoice {
|
||||
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 rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -342,6 +343,9 @@ impl ProductBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Product {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Product {
|
||||
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 super::db::Model; // Removed old Model trait import
|
||||
use chrono::{DateTime, Utc};
|
||||
@ -566,6 +566,9 @@ impl SaleBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Sale {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Sale {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
@ -491,6 +491,10 @@ impl ServiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Service {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Service {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 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
|
||||
impl Model for Circle {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 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
|
||||
impl Model for Member {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
|
||||
/// Record types for a DNS record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -57,6 +57,10 @@ impl Name {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Name {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Name {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Asset represents a cryptocurrency asset in a wallet
|
||||
@ -70,7 +70,8 @@ impl Wallet {
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Wallet {}
|
||||
impl Storable for Wallet {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
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::Resolution;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
@ -87,7 +89,7 @@ pub struct Company {
|
||||
// Removed shareholders property
|
||||
}
|
||||
|
||||
|
||||
impl Storable for Company{}
|
||||
|
||||
// Model requires get_id and db_prefix
|
||||
impl Model for Company {
|
||||
@ -150,24 +152,22 @@ impl Company {
|
||||
}
|
||||
|
||||
/// 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
|
||||
// For now, we'll just update the timestamp to indicate the change
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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
|
||||
// For now, we'll just update the timestamp to indicate the change
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all resolutions for this company
|
||||
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<super::Resolution>> {
|
||||
let all_resolutions = db.list::<super::Resolution>()?;
|
||||
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<Resolution>> {
|
||||
let all_resolutions = db.list::<Resolution>()?;
|
||||
let company_resolutions = all_resolutions
|
||||
.into_iter()
|
||||
.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
|
||||
impl Model for Meeting {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -6,7 +6,6 @@ pub mod vote;
|
||||
pub mod resolution;
|
||||
// All modules:
|
||||
pub mod committee;
|
||||
pub mod compliance;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use company::{Company, CompanyStatus, BusinessType};
|
||||
@ -16,7 +15,6 @@ pub use user::User;
|
||||
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
||||
pub use resolution::{Resolution, ResolutionStatus, Approval};
|
||||
pub use committee::{Committee, CommitteeMember, CommitteeRole};
|
||||
pub use compliance::{ComplianceRequirement, ComplianceDocument, ComplianceAudit};
|
||||
|
||||
// Re-export database components from db module
|
||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult};
|
@ -180,6 +180,9 @@ impl Resolution {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Storable for Resolution{}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Resolution {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -63,6 +63,8 @@ impl Shareholder {
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Shareholder{}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Shareholder {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -42,6 +42,8 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for User{}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for User {
|
||||
fn get_id(&self) -> u32 {
|
||||
|
@ -51,7 +51,7 @@ pub struct Ballot {
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// Removed old Model trait implementation
|
||||
impl Storable for Vote{}
|
||||
|
||||
impl Vote {
|
||||
/// Create a new vote with default timestamps
|
||||
|
Loading…
Reference in New Issue
Block a user