use std::collections::HashSet;
use axum::Json;
use axum::extract::{Path, Query, State};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use vti_common::error::AppError;
use vti_common::pagination::{Cursor, MAX_LIMIT, Paginated};
use crate::auth::AdminAuth;
use crate::policy::{
Policy, PolicyPurpose, get_active_policy_id, get_policy, list_policies_paginated,
};
use crate::server::AppState;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PolicyResponse {
#[serde(flatten)]
pub policy: Policy,
pub is_active: bool,
}
impl PolicyResponse {
fn from_policy(policy: Policy, is_active: bool) -> Self {
Self { policy, is_active }
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListPoliciesQuery {
pub purpose: Option<PolicyPurpose>,
pub status: Option<PolicyStatusFilter>,
pub cursor: Option<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PolicyStatusFilter {
Active,
Archived,
}
pub async fn list_policies(
_auth: AdminAuth,
State(state): State<AppState>,
Query(query): Query<ListPoliciesQuery>,
) -> Result<Json<Paginated<PolicyResponse>>, AppError> {
let limit = query.limit.unwrap_or(50).clamp(1, MAX_LIMIT);
let audit_writer = state
.audit_writer
.as_ref()
.ok_or_else(|| AppError::Internal("audit_writer not initialised".into()))?;
let audit_key = audit_writer.active_key().await?;
let decoded_cursor = match &query.cursor {
Some(s) => Some(Cursor::decode(s, &audit_key.key)?),
None => None,
};
let page = list_policies_paginated(
&state.policies_ks,
&audit_key,
decoded_cursor.as_ref(),
limit,
)
.await?;
let mut active_ids: HashSet<Uuid> = HashSet::new();
for purpose in PolicyPurpose::ALL {
if let Some(id) = get_active_policy_id(&state.active_policies_ks, purpose).await? {
active_ids.insert(id);
}
}
let purpose_filter = query.purpose;
let status_filter = query.status;
let items: Vec<PolicyResponse> = page
.items
.into_iter()
.filter(|p| purpose_filter.is_none_or(|f| p.purpose == f))
.filter_map(|p| {
let is_active = active_ids.contains(&p.id);
match status_filter {
Some(PolicyStatusFilter::Active) if !is_active => return None,
Some(PolicyStatusFilter::Archived) if is_active => return None,
_ => {}
}
Some(PolicyResponse::from_policy(p, is_active))
})
.collect();
Ok(Json(Paginated {
items,
next_cursor: page.next_cursor,
total_estimate: page.total_estimate,
}))
}
pub async fn show_policy(
_auth: AdminAuth,
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<PolicyResponse>, AppError> {
let policy = get_policy(&state.policies_ks, id)
.await?
.ok_or_else(|| AppError::NotFound(format!("policy not found: {id}")))?;
let active_id = get_active_policy_id(&state.active_policies_ks, policy.purpose).await?;
let is_active = active_id == Some(id);
Ok(Json(PolicyResponse::from_policy(policy, is_active)))
}