did-hosting-server 0.8.3

DID Hosting Server, handles management and serving of WebVH Content
Documentation
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use tracing::{info, warn};

use crate::acl::{AclEntry, delete_acl_entry, get_acl_entry, list_acl_entries, store_acl_entry};
use crate::auth::AdminAuth;
use crate::auth::session::now_epoch;
use crate::error::AppError;
use crate::server::AppState;
use did_hosting_common::server::acl::{
    AclEntryResponse, AclListResponse, CreateAclRequest, UpdateAclRequest,
};

// ---------- GET /acl ----------

pub async fn list_acl(
    auth: AdminAuth,
    State(state): State<AppState>,
) -> Result<Json<AclListResponse>, AppError> {
    let entries: Vec<AclEntryResponse> = list_acl_entries(&state.acl_ks)
        .await?
        .into_iter()
        .map(AclEntryResponse::from)
        .collect();
    info!(caller = %auth.0.did, count = entries.len(), "ACL listed");
    Ok(Json(AclListResponse { entries }))
}

// ---------- POST /acl ----------

pub async fn create_acl(
    auth: AdminAuth,
    State(state): State<AppState>,
    Json(req): Json<CreateAclRequest>,
) -> Result<(StatusCode, Json<AclEntryResponse>), AppError> {
    // Check for duplicates
    if get_acl_entry(&state.acl_ks, &req.did).await?.is_some() {
        warn!(caller = %auth.0.did, target_did = %req.did, "ACL create rejected: entry already exists");
        return Err(AppError::Conflict(format!(
            "ACL entry already exists for DID: {}",
            req.did
        )));
    }

    let entry = AclEntry {
        did: req.did,
        role: req.role,
        label: req.label,
        created_at: now_epoch(),
        max_total_size: req.max_total_size,
        max_did_count: req.max_did_count,

        domains: did_hosting_common::server::domain::DomainScope::All,
    };

    store_acl_entry(&state.acl_ks, &entry).await?;

    info!(caller = %auth.0.did, did = %entry.did, role = %entry.role, "ACL entry created");
    Ok((StatusCode::CREATED, Json(AclEntryResponse::from(entry))))
}

// ---------- PUT /acl/{did} ----------

pub async fn update_acl(
    auth: AdminAuth,
    State(state): State<AppState>,
    Path(did): Path<String>,
    Json(req): Json<UpdateAclRequest>,
) -> Result<Json<AclEntryResponse>, AppError> {
    let mut entry = get_acl_entry(&state.acl_ks, &did)
        .await?
        .ok_or_else(|| AppError::NotFound(format!("ACL entry not found for DID: {did}")))?;

    if let Some(role) = req.role {
        entry.role = role;
    }
    if let Some(label) = req.label {
        entry.label = Some(label);
    }
    if req.max_total_size.is_some() {
        entry.max_total_size = req.max_total_size;
    }
    if req.max_did_count.is_some() {
        entry.max_did_count = req.max_did_count;
    }

    store_acl_entry(&state.acl_ks, &entry).await?;

    info!(
        caller = %auth.0.did,
        did = %did,
        max_total_size = ?entry.max_total_size,
        max_did_count = ?entry.max_did_count,
        label = ?entry.label,
        "ACL entry updated"
    );
    Ok(Json(AclEntryResponse::from(entry)))
}

// ---------- DELETE /acl/{did} ----------

pub async fn delete_acl(
    auth: AdminAuth,
    State(state): State<AppState>,
    Path(did): Path<String>,
) -> Result<StatusCode, AppError> {
    // Prevent self-deletion
    if auth.0.did == did {
        warn!(caller = %auth.0.did, "ACL delete rejected: attempted self-deletion");
        return Err(AppError::Conflict(
            "cannot delete your own ACL entry".into(),
        ));
    }

    // Verify entry exists
    get_acl_entry(&state.acl_ks, &did)
        .await?
        .ok_or_else(|| AppError::NotFound(format!("ACL entry not found for DID: {did}")))?;

    delete_acl_entry(&state.acl_ks, &did).await?;

    info!(caller = %auth.0.did, did = %did, "ACL entry deleted");
    Ok(StatusCode::NO_CONTENT)
}