This commit is contained in:
despiegk 2025-04-20 09:21:32 +02:00
parent 4b2e8ca6b9
commit c956db8adf
36 changed files with 4391 additions and 229 deletions

2017
acldb/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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(&params.right) {
Ok(right) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_update(&params.caller_pubkey, &params.name, &params.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(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_remove(&params.caller_pubkey, &params.name, &params.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(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_del(&params.caller_pubkey, &params.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(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
let topic = db.topic(&params.topic);
match base64_decode(&params.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
View 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
View 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
View 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"
}
}
}
}
}
}

View 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>

View 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(())
}

View 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(())
}

View 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!");
}

View File

@ -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);

View File

@ -235,6 +235,10 @@ impl ContractBuilder {
}
}
impl Storable for Contract {}
// Implement Model trait
impl Model for Contract {
fn get_id(&self) -> u32 {

View File

@ -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 {

View File

@ -133,6 +133,9 @@ impl CustomerBuilder {
}
}
impl Storable for Customer {}
// Implement Model trait
impl Model for Customer {
fn get_id(&self) -> u32 {

View File

@ -91,6 +91,9 @@ impl ExchangeRateBuilder {
}
}
impl Storable for ExchangeRate {}
// Implement Model trait
impl Model for ExchangeRate {
fn get_id(&self) -> u32 {

View File

@ -500,6 +500,8 @@ impl InvoiceBuilder {
}
}
impl Storable for Invoice {}
// Implement Model trait
impl Model for Invoice {
fn get_id(&self) -> u32 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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"
}
}

View File

@ -175,6 +175,7 @@ impl Meeting {
}
}
impl Storable for Meeting{}
// Implement Model trait
impl Model for Meeting {
fn get_id(&self) -> u32 {

View File

@ -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};

View File

@ -180,6 +180,9 @@ impl Resolution {
}
}
impl Storable for Resolution{}
// Implement Model trait
impl Model for Resolution {
fn get_id(&self) -> u32 {

View File

@ -63,6 +63,8 @@ impl Shareholder {
}
}
impl Storable for Shareholder{}
// Implement Model trait
impl Model for Shareholder {
fn get_id(&self) -> u32 {

View File

@ -42,6 +42,8 @@ impl User {
}
}
impl Storable for User{}
// Implement Model trait
impl Model for User {
fn get_id(&self) -> u32 {

View File

@ -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