AkurAI Build
Menu

Public repository / ce67cb1adf4fc35e95bdefe771da396be30e9615

AkurAI-PassVault / crates/passvault-core/src/entities/mod.rs

Latest change 2c509452a4982321959914cab8d52b881e8368de - AKURAI-PASSVAULT-6 cargo fmt --all by Ólafur Búi Ólafsson

use crate::error::Error;
use crate::types::Timestamp;
use akurai_json::Value;

pub fn normalize_entry_name(name: &str) -> Result<String, Error> {
    let trimmed = name.trim();
    if trimmed.is_empty() {
        return Err(Error::Validation("name is required".into()));
    }
    Ok(trimmed.to_string())
}

#[derive(Debug, Clone)]
pub struct VaultEntry {
    pub id: i64,
    pub name: String,
    pub username: String,
    pub password: String,
    pub uri: String,
    pub notes: String,
    pub folder_id: Option<i64>,
    pub created_at: Timestamp,
    pub updated_at: Timestamp,
}

impl VaultEntry {
    pub fn from_value(v: &Value) -> Result<Self, Error> {
        Ok(VaultEntry {
            id: v
                .get("id")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing id".into()))?,
            name: v
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            username: v
                .get("username")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            password: v
                .get("password")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            uri: v
                .get("uri")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            notes: v
                .get("notes")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            folder_id: v.get("folderId").and_then(|v| v.as_i64()),
            created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
            updated_at: v.get("updatedAt").and_then(|v| v.as_i64()).unwrap_or(0),
        })
    }

    pub fn to_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            ("name".into(), Value::Str(self.name.clone())),
            ("username".into(), Value::Str(self.username.clone())),
            ("password".into(), Value::Str(self.password.clone())),
            ("uri".into(), Value::Str(self.uri.clone())),
            ("notes".into(), Value::Str(self.notes.clone())),
            (
                "folderId".into(),
                self.folder_id.map_or(Value::Null, Value::Int),
            ),
            ("createdAt".into(), Value::Int(self.created_at)),
            ("updatedAt".into(), Value::Int(self.updated_at)),
        ])
    }

    pub fn summary_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            ("name".into(), Value::Str(self.name.clone())),
            ("username".into(), Value::Str(self.username.clone())),
            ("uri".into(), Value::Str(self.uri.clone())),
            (
                "folderId".into(),
                self.folder_id.map_or(Value::Null, Value::Int),
            ),
        ])
    }
}

#[derive(Debug, Clone)]
pub struct Folder {
    pub id: i64,
    pub name: String,
    pub created_at: Timestamp,
}

impl Folder {
    pub fn from_value(v: &Value) -> Result<Self, Error> {
        Ok(Folder {
            id: v
                .get("id")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing id".into()))?,
            name: v
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
        })
    }

    pub fn to_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            ("name".into(), Value::Str(self.name.clone())),
            ("createdAt".into(), Value::Int(self.created_at)),
        ])
    }
}

/// A tenant-scoped managed secret for a workspace.
#[derive(Debug, Clone)]
pub struct WorkspaceSecret {
    pub id: i64,
    pub organization_id: String,
    pub workspace_id: String,
    pub name: String,
    pub value: String,
    pub notes: String,
    pub active: bool,
    pub rotation_id: Option<i64>,
    pub created_at: Timestamp,
    pub updated_at: Timestamp,
}

impl WorkspaceSecret {
    pub fn from_value(v: &Value) -> Result<Self, Error> {
        Ok(WorkspaceSecret {
            id: v
                .get("id")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing id".into()))?,
            organization_id: v
                .get("organizationId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            workspace_id: v
                .get("workspaceId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            name: v
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            value: v
                .get("value")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            notes: v
                .get("notes")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            active: v.get("active").and_then(|v| v.as_bool()).unwrap_or(true),
            rotation_id: v.get("rotationId").and_then(|v| v.as_i64()),
            created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
            updated_at: v.get("updatedAt").and_then(|v| v.as_i64()).unwrap_or(0),
        })
    }

    pub fn to_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            (
                "organizationId".into(),
                Value::Str(self.organization_id.clone()),
            ),
            ("workspaceId".into(), Value::Str(self.workspace_id.clone())),
            ("name".into(), Value::Str(self.name.clone())),
            ("value".into(), Value::Str(self.value.clone())),
            ("notes".into(), Value::Str(self.notes.clone())),
            ("active".into(), Value::Bool(self.active)),
            (
                "rotationId".into(),
                self.rotation_id.map_or(Value::Null, Value::Int),
            ),
            ("createdAt".into(), Value::Int(self.created_at)),
            ("updatedAt".into(), Value::Int(self.updated_at)),
        ])
    }

    /// Metadata-only view — never includes the secret value.
    pub fn metadata_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            (
                "organizationId".into(),
                Value::Str(self.organization_id.clone()),
            ),
            ("workspaceId".into(), Value::Str(self.workspace_id.clone())),
            ("name".into(), Value::Str(self.name.clone())),
            ("notes".into(), Value::Str(self.notes.clone())),
            ("active".into(), Value::Bool(self.active)),
            (
                "rotationId".into(),
                self.rotation_id.map_or(Value::Null, Value::Int),
            ),
            ("createdAt".into(), Value::Int(self.created_at)),
            ("updatedAt".into(), Value::Int(self.updated_at)),
        ])
    }
}

/// A service identity within a workspace, used for scoped secret access.
#[derive(Debug, Clone)]
pub struct ServiceIdentity {
    pub id: i64,
    pub organization_id: String,
    pub workspace_id: String,
    pub name: String,
    pub created_at: Timestamp,
}

impl ServiceIdentity {
    pub fn from_value(v: &Value) -> Result<Self, Error> {
        Ok(ServiceIdentity {
            id: v
                .get("id")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing id".into()))?,
            organization_id: v
                .get("organizationId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            workspace_id: v
                .get("workspaceId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            name: v
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
        })
    }

    pub fn to_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            (
                "organizationId".into(),
                Value::Str(self.organization_id.clone()),
            ),
            ("workspaceId".into(), Value::Str(self.workspace_id.clone())),
            ("name".into(), Value::Str(self.name.clone())),
            ("createdAt".into(), Value::Int(self.created_at)),
        ])
    }
}

/// A grant linking a service identity to a secret with a visibility level.
#[derive(Debug, Clone)]
pub struct SecretGrant {
    pub id: i64,
    pub organization_id: String,
    pub workspace_id: String,
    pub identity_id: i64,
    pub secret_id: i64,
    pub visibility: String,
    pub created_at: Timestamp,
}

impl SecretGrant {
    pub fn from_value(v: &Value) -> Result<Self, Error> {
        Ok(SecretGrant {
            id: v
                .get("id")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing id".into()))?,
            organization_id: v
                .get("organizationId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            workspace_id: v
                .get("workspaceId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            identity_id: v
                .get("identityId")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing identityId".into()))?,
            secret_id: v
                .get("secretId")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing secretId".into()))?,
            visibility: v
                .get("visibility")
                .and_then(|v| v.as_str())
                .unwrap_or("metadata")
                .to_string(),
            created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
        })
    }

    pub fn to_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            (
                "organizationId".into(),
                Value::Str(self.organization_id.clone()),
            ),
            ("workspaceId".into(), Value::Str(self.workspace_id.clone())),
            ("identityId".into(), Value::Int(self.identity_id)),
            ("secretId".into(), Value::Int(self.secret_id)),
            ("visibility".into(), Value::Str(self.visibility.clone())),
            ("createdAt".into(), Value::Int(self.created_at)),
        ])
    }
}

/// Rotation state for a secret — supports overlap and rollback.
#[derive(Debug, Clone)]
pub struct RotationState {
    pub id: i64,
    pub secret_id: i64,
    pub previous_value_hash: Option<String>,
    pub phase: String,
    pub activated_at: Timestamp,
    pub overlap_until: Option<Timestamp>,
    pub created_at: Timestamp,
}

impl RotationState {
    pub fn from_value(v: &Value) -> Result<Self, Error> {
        Ok(RotationState {
            id: v
                .get("id")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing id".into()))?,
            secret_id: v
                .get("secretId")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing secretId".into()))?,
            previous_value_hash: v
                .get("previousValueHash")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string()),
            phase: v
                .get("phase")
                .and_then(|v| v.as_str())
                .unwrap_or("active")
                .to_string(),
            activated_at: v.get("activatedAt").and_then(|v| v.as_i64()).unwrap_or(0),
            overlap_until: v.get("overlapUntil").and_then(|v| v.as_i64()),
            created_at: v.get("createdAt").and_then(|v| v.as_i64()).unwrap_or(0),
        })
    }

    pub fn to_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            ("secretId".into(), Value::Int(self.secret_id)),
            (
                "previousValueHash".into(),
                self.previous_value_hash
                    .as_ref()
                    .map_or(Value::Null, |s| Value::Str(s.clone())),
            ),
            ("phase".into(), Value::Str(self.phase.clone())),
            ("activatedAt".into(), Value::Int(self.activated_at)),
            (
                "overlapUntil".into(),
                self.overlap_until.map_or(Value::Null, Value::Int),
            ),
            ("createdAt".into(), Value::Int(self.created_at)),
        ])
    }
}

/// Immutable audit entry — tracks actor/purpose/result without secret material.
#[derive(Debug, Clone)]
pub struct AuditEntry {
    pub id: i64,
    pub organization_id: String,
    pub workspace_id: String,
    pub actor: String,
    pub action: String,
    pub target: String,
    pub purpose: String,
    pub result: String,
    pub timestamp: Timestamp,
}

impl AuditEntry {
    pub fn from_value(v: &Value) -> Result<Self, Error> {
        Ok(AuditEntry {
            id: v
                .get("id")
                .and_then(|v| v.as_i64())
                .ok_or(Error::Validation("missing id".into()))?,
            organization_id: v
                .get("organizationId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            workspace_id: v
                .get("workspaceId")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            actor: v
                .get("actor")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            action: v
                .get("action")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            target: v
                .get("target")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            purpose: v
                .get("purpose")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            result: v
                .get("result")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string(),
            timestamp: v.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0),
        })
    }

    pub fn to_value(&self) -> Value {
        Value::Object(vec![
            ("id".into(), Value::Int(self.id)),
            (
                "organizationId".into(),
                Value::Str(self.organization_id.clone()),
            ),
            ("workspaceId".into(), Value::Str(self.workspace_id.clone())),
            ("actor".into(), Value::Str(self.actor.clone())),
            ("action".into(), Value::Str(self.action.clone())),
            ("target".into(), Value::Str(self.target.clone())),
            ("purpose".into(), Value::Str(self.purpose.clone())),
            ("result".into(), Value::Str(self.result.clone())),
            ("timestamp".into(), Value::Int(self.timestamp)),
        ])
    }
}

#[cfg(test)]
mod tests {
    use super::normalize_entry_name;

    #[test]
    fn normalize_entry_name_trims_whitespace() {
        assert_eq!(normalize_entry_name("  Example  ").unwrap(), "Example");
    }

    #[test]
    fn normalize_entry_name_rejects_blank_values() {
        assert!(normalize_entry_name(" \t\n ").is_err());
    }
}