use chrono::Utc;
use serde_json::Value;
use tracing::info;
use vta_sdk::did_templates::{DidTemplate, DidTemplateRecord, Scope, TemplateVars};
use vti_common::identifier::validate_identifier;
use crate::audit;
use crate::auth::AuthClaims;
use crate::did_templates as store;
use crate::error::AppError;
use crate::store::KeyspaceHandle;
pub async fn create_global(
templates_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
auth: &AuthClaims,
template: DidTemplate,
channel: &str,
) -> Result<DidTemplateRecord, AppError> {
auth.require_super_admin()?;
template
.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
validate_identifier("template name", &template.name)?;
if store::get_global_template(templates_ks, &template.name)
.await?
.is_some()
{
return Err(AppError::Conflict(format!(
"template already exists: {}",
template.name
)));
}
let now = unix_secs();
let record = DidTemplateRecord {
template,
scope: Scope::Global,
created_at: now,
updated_at: now,
created_by: auth.did.clone(),
};
store::store_global_template(templates_ks, &record).await?;
let _ = audit::record(
audit_ks,
"did_template.created",
&auth.did,
Some(&record.template.name),
"success",
Some(channel),
None,
)
.await;
info!(channel, name = %record.template.name, "did template created");
Ok(record)
}
pub async fn update_global(
templates_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
auth: &AuthClaims,
name: &str,
template: DidTemplate,
channel: &str,
) -> Result<DidTemplateRecord, AppError> {
auth.require_super_admin()?;
validate_identifier("template name", name)?;
if template.name != name {
return Err(AppError::Validation(format!(
"template name in body ('{}') does not match path ('{name}')",
template.name
)));
}
template
.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
let existing = store::get_global_template(templates_ks, name)
.await?
.ok_or_else(|| AppError::NotFound(format!("template not found: {name}")))?;
let record = DidTemplateRecord {
template,
scope: Scope::Global,
created_at: existing.created_at,
updated_at: unix_secs(),
created_by: existing.created_by,
};
store::store_global_template(templates_ks, &record).await?;
let _ = audit::record(
audit_ks,
"did_template.updated",
&auth.did,
Some(&record.template.name),
"success",
Some(channel),
None,
)
.await;
info!(channel, name = %record.template.name, "did template updated");
Ok(record)
}
pub async fn get_global(
templates_ks: &KeyspaceHandle,
_auth: &AuthClaims,
name: &str,
_channel: &str,
) -> Result<DidTemplateRecord, AppError> {
store::get_global_template(templates_ks, name)
.await?
.ok_or_else(|| AppError::NotFound(format!("template not found: {name}")))
}
pub async fn list_global(
templates_ks: &KeyspaceHandle,
_auth: &AuthClaims,
_channel: &str,
) -> Result<Vec<DidTemplateRecord>, AppError> {
store::list_global_templates(templates_ks).await
}
pub async fn delete_global(
templates_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
auth: &AuthClaims,
name: &str,
channel: &str,
) -> Result<(), AppError> {
auth.require_super_admin()?;
if store::get_global_template(templates_ks, name)
.await?
.is_none()
{
return Err(AppError::NotFound(format!("template not found: {name}")));
}
store::delete_global_template(templates_ks, name).await?;
let _ = audit::record(
audit_ks,
"did_template.deleted",
&auth.did,
Some(name),
"success",
Some(channel),
None,
)
.await;
info!(channel, name, "did template deleted");
Ok(())
}
pub async fn render_global(
templates_ks: &KeyspaceHandle,
config: &crate::config::AppConfig,
_auth: &AuthClaims,
name: &str,
caller_vars: TemplateVars,
_channel: &str,
) -> Result<Value, AppError> {
let record = store::get_global_template(templates_ks, name)
.await?
.ok_or_else(|| AppError::NotFound(format!("template not found: {name}")))?;
let mut vars = ambient_vars(config);
vars.extend(caller_vars);
record
.template
.render(&vars)
.map_err(|e| AppError::Validation(e.to_string()))
}
fn ambient_vars(config: &crate::config::AppConfig) -> TemplateVars {
let mut vars = TemplateVars::new();
if let Some(vta_did) = config.vta_did.as_deref() {
vars.insert_string("VTA_DID", vta_did);
}
if let Some(vta_url) = config.public_url.as_deref() {
vars.insert_string("VTA_URL", vta_url);
}
vars.insert_string("NOW", Utc::now().to_rfc3339());
vars
}
async fn ambient_vars_with_context(
config: &crate::config::AppConfig,
contexts_ks: &KeyspaceHandle,
context_id: &str,
) -> TemplateVars {
let mut vars = ambient_vars(config);
vars.insert_string("CONTEXT_ID", context_id);
if let Ok(Some(ctx)) = crate::contexts::get_context(contexts_ks, context_id).await
&& let Some(ref did) = ctx.did
{
vars.insert_string("CONTEXT_DID", did.clone());
}
vars
}
fn require_context_write(auth: &AuthClaims, context_id: &str) -> Result<(), AppError> {
if auth.is_super_admin() {
return Ok(());
}
auth.require_admin()?;
auth.require_context(context_id)?;
Ok(())
}
fn require_context_read(auth: &AuthClaims, context_id: &str) -> Result<(), AppError> {
auth.require_context(context_id)
}
pub async fn create_context(
templates_ks: &KeyspaceHandle,
contexts_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
auth: &AuthClaims,
context_id: &str,
template: DidTemplate,
channel: &str,
) -> Result<DidTemplateRecord, AppError> {
require_context_write(auth, context_id)?;
validate_identifier("context id", context_id)?;
validate_identifier("template name", &template.name)?;
if crate::contexts::get_context(contexts_ks, context_id)
.await?
.is_none()
{
return Err(AppError::NotFound(format!(
"context not found: {context_id}"
)));
}
template
.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
if store::get_context_template(templates_ks, context_id, &template.name)
.await?
.is_some()
{
return Err(AppError::Conflict(format!(
"template already exists in context '{context_id}': {}",
template.name
)));
}
let now = unix_secs();
let record = DidTemplateRecord {
template,
scope: Scope::Context {
context_id: context_id.to_string(),
},
created_at: now,
updated_at: now,
created_by: auth.did.clone(),
};
store::store_context_template(templates_ks, context_id, &record).await?;
let _ = audit::record(
audit_ks,
"did_template.created",
&auth.did,
Some(&record.template.name),
"success",
Some(channel),
Some(context_id),
)
.await;
info!(
channel,
context_id,
name = %record.template.name,
"did template created (context scope)"
);
Ok(record)
}
pub async fn update_context(
templates_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
auth: &AuthClaims,
context_id: &str,
name: &str,
template: DidTemplate,
channel: &str,
) -> Result<DidTemplateRecord, AppError> {
require_context_write(auth, context_id)?;
validate_identifier("context id", context_id)?;
validate_identifier("template name", name)?;
if template.name != name {
return Err(AppError::Validation(format!(
"template name in body ('{}') does not match path ('{name}')",
template.name
)));
}
template
.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
let existing = store::get_context_template(templates_ks, context_id, name)
.await?
.ok_or_else(|| {
AppError::NotFound(format!(
"template not found in context '{context_id}': {name}"
))
})?;
let record = DidTemplateRecord {
template,
scope: Scope::Context {
context_id: context_id.to_string(),
},
created_at: existing.created_at,
updated_at: unix_secs(),
created_by: existing.created_by,
};
store::store_context_template(templates_ks, context_id, &record).await?;
let _ = audit::record(
audit_ks,
"did_template.updated",
&auth.did,
Some(&record.template.name),
"success",
Some(channel),
Some(context_id),
)
.await;
info!(
channel,
context_id,
name = %record.template.name,
"did template updated (context scope)"
);
Ok(record)
}
pub async fn get_context(
templates_ks: &KeyspaceHandle,
auth: &AuthClaims,
context_id: &str,
name: &str,
_channel: &str,
) -> Result<DidTemplateRecord, AppError> {
require_context_read(auth, context_id)?;
store::get_context_template(templates_ks, context_id, name)
.await?
.ok_or_else(|| {
AppError::NotFound(format!(
"template not found in context '{context_id}': {name}"
))
})
}
pub async fn list_context(
templates_ks: &KeyspaceHandle,
auth: &AuthClaims,
context_id: &str,
_channel: &str,
) -> Result<Vec<DidTemplateRecord>, AppError> {
require_context_read(auth, context_id)?;
store::list_context_templates(templates_ks, context_id).await
}
pub async fn delete_context(
templates_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
auth: &AuthClaims,
context_id: &str,
name: &str,
channel: &str,
) -> Result<(), AppError> {
require_context_write(auth, context_id)?;
if store::get_context_template(templates_ks, context_id, name)
.await?
.is_none()
{
return Err(AppError::NotFound(format!(
"template not found in context '{context_id}': {name}"
)));
}
store::delete_context_template(templates_ks, context_id, name).await?;
let _ = audit::record(
audit_ks,
"did_template.deleted",
&auth.did,
Some(name),
"success",
Some(channel),
Some(context_id),
)
.await;
info!(
channel,
context_id, name, "did template deleted (context scope)"
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn render_context(
templates_ks: &KeyspaceHandle,
contexts_ks: &KeyspaceHandle,
config: &crate::config::AppConfig,
auth: &AuthClaims,
context_id: &str,
name: &str,
caller_vars: TemplateVars,
_channel: &str,
) -> Result<Value, AppError> {
require_context_read(auth, context_id)?;
let record = store::get_context_template(templates_ks, context_id, name)
.await?
.ok_or_else(|| {
AppError::NotFound(format!(
"template not found in context '{context_id}': {name}"
))
})?;
let mut vars = ambient_vars_with_context(config, contexts_ks, context_id).await;
vars.extend(caller_vars);
record
.template
.render(&vars)
.map_err(|e| AppError::Validation(e.to_string()))
}
fn unix_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}