feat(cli/org): org add — collection-scoped typed item create with grant guard
This commit is contained in:
@@ -6,7 +6,8 @@ use std::path::Path;
|
||||
use anyhow::{Context, Result};
|
||||
use relicario_core::{
|
||||
generate_org_key, wrap_org_key,
|
||||
CollectionDef, MemberId, OrgCollections, OrgManifest, OrgMembers, OrgMeta, OrgRole, OrgMember,
|
||||
CollectionDef, Item, ItemCore, MemberId, OrgCollections, OrgManifest, OrgMembers, OrgMeta,
|
||||
OrgRole, OrgMember,
|
||||
encrypt_org_manifest,
|
||||
};
|
||||
|
||||
@@ -744,6 +745,132 @@ pub fn run_audit(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Item kinds `org add` supports without interactive prompts.
|
||||
pub enum OrgAddKind {
|
||||
Login {
|
||||
title: String,
|
||||
username: Option<String>,
|
||||
url: Option<String>,
|
||||
password: Option<String>,
|
||||
},
|
||||
SecureNote {
|
||||
title: String,
|
||||
body: String,
|
||||
},
|
||||
Identity {
|
||||
title: String,
|
||||
full_name: Option<String>,
|
||||
email: Option<String>,
|
||||
phone: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn build_org_item(kind: OrgAddKind, tags: Vec<String>) -> Result<Item> {
|
||||
use relicario_core::item_types::{IdentityCore, LoginCore, SecureNoteCore};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
let mut item = match kind {
|
||||
OrgAddKind::Login { title, username, url, password } => {
|
||||
let parsed_url = match url {
|
||||
Some(s) => Some(url::Url::parse(&s).with_context(|| format!("invalid URL: {s}"))?),
|
||||
None => None,
|
||||
};
|
||||
let password = password.map(Zeroizing::new);
|
||||
Item::new(title, ItemCore::Login(LoginCore {
|
||||
username,
|
||||
password,
|
||||
url: parsed_url,
|
||||
totp: None,
|
||||
}))
|
||||
}
|
||||
OrgAddKind::SecureNote { title, body } => {
|
||||
Item::new(title, ItemCore::SecureNote(SecureNoteCore {
|
||||
body: Zeroizing::new(body),
|
||||
}))
|
||||
}
|
||||
OrgAddKind::Identity { title, full_name, email, phone } => {
|
||||
Item::new(title, ItemCore::Identity(IdentityCore {
|
||||
full_name,
|
||||
address: None,
|
||||
phone,
|
||||
email,
|
||||
date_of_birth: None,
|
||||
}))
|
||||
}
|
||||
};
|
||||
item.tags = tags;
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
pub fn run_add(dir: &Path, collection: &str, kind: OrgAddKind, tags: Vec<String>) -> Result<()> {
|
||||
use crate::org_session::UnlockedOrgVault;
|
||||
|
||||
let vault = crate::org_session::open_org_vault(Some(dir))?;
|
||||
let caller = vault.current_member()?;
|
||||
|
||||
// Slug must exist in collections.json…
|
||||
let collections = vault.load_collections()?;
|
||||
if !collections.contains_slug(collection) {
|
||||
anyhow::bail!("collection `{collection}` does not exist — create it with `relicario org create-collection`");
|
||||
}
|
||||
// …and the caller must hold a grant for it.
|
||||
UnlockedOrgVault::ensure_grant(&caller, collection)?;
|
||||
|
||||
let item = build_org_item(kind, tags)?;
|
||||
let item_rel = vault.save_item(collection, &item)?;
|
||||
|
||||
// Upsert the manifest entry (collection slug stored plaintext inside the
|
||||
// encrypted manifest).
|
||||
let mut manifest = vault.load_manifest()?;
|
||||
upsert_org_entry(&mut manifest, &item, collection);
|
||||
vault.save_manifest(&manifest)?;
|
||||
|
||||
let subject = format!(
|
||||
"org add: {} ({})",
|
||||
crate::helpers::sanitize_for_commit(&item.title),
|
||||
item.id.as_str()
|
||||
);
|
||||
let commit_msg = format!(
|
||||
"{subject}\n\nRelicario-Actor: {} {}\nRelicario-Action: item-create\nRelicario-Collection: {}\nRelicario-Item: {}",
|
||||
caller.display_name,
|
||||
caller.member_id.as_str(),
|
||||
collection,
|
||||
item.id.as_str()
|
||||
);
|
||||
crate::org_session::org_git_run(
|
||||
&vault.root,
|
||||
&["add", &item_rel, "manifest.enc"],
|
||||
"org add: git add",
|
||||
)?;
|
||||
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "org add: git commit")?;
|
||||
|
||||
println!("Added {} ({}) to `{}`", item.title, item.id.as_str(), collection);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert-or-replace an `OrgManifestEntry` mirroring the personal-vault
|
||||
/// `Manifest::upsert`. Keyed by item id.
|
||||
fn upsert_org_entry(
|
||||
manifest: &mut relicario_core::OrgManifest,
|
||||
item: &Item,
|
||||
collection: &str,
|
||||
) {
|
||||
let entry = relicario_core::OrgManifestEntry {
|
||||
id: item.id.clone(),
|
||||
r#type: item.r#type,
|
||||
title: item.title.clone(),
|
||||
tags: item.tags.clone(),
|
||||
modified: item.modified,
|
||||
trashed_at: item.trashed_at,
|
||||
collection: collection.to_string(),
|
||||
};
|
||||
if let Some(slot) = manifest.entries.iter_mut().find(|e| e.id == item.id) {
|
||||
*slot = entry;
|
||||
} else {
|
||||
manifest.entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user