use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::HeaderMap;
use axum::response::{IntoResponse, Response};
use axum::Extension;
use sbol_db_app::PermissionService;
use serde::Deserialize;
use super::routes::{public_uri, user_uri, PublicObject, UserObject};
use super::CurrentUser;
use crate::{ApiError, AppState};
fn service(state: &AppState) -> PermissionService {
PermissionService::new(
state.app.sparql_update.clone(),
state.app.acl_service.clone(),
)
}
fn require_user(user: Option<sbol_db_core::User>) -> Result<sbol_db_core::User, ApiError> {
user.ok_or_else(|| ApiError::Forbidden("authentication is required to share".to_owned()))
}
fn success() -> Response {
(axum::http::StatusCode::OK, "Success").into_response()
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct AddOwnerForm {
pub user: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserRemoveOwnerPath {
pub user_id: String,
pub collection_id: String,
pub display_id: String,
pub version: String,
pub username: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicRemoveOwnerPath {
pub collection_id: String,
pub display_id: String,
pub version: String,
pub username: String,
}
pub async fn user_add_owner(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<UserObject>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let uri = user_uri(&state, &object);
add_owner(state, user, uri, &headers, &body).await
}
pub async fn public_add_owner(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<PublicObject>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let uri = public_uri(&state, &object);
add_owner(state, user, uri, &headers, &body).await
}
pub async fn user_remove_owner(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<UserRemoveOwnerPath>,
) -> Result<Response, ApiError> {
let uri = format!(
"{}user/{}/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.user_id,
p.collection_id,
p.display_id,
p.version
);
remove_owner(state, user, uri, &p.username).await
}
pub async fn public_remove_owner(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<PublicRemoveOwnerPath>,
) -> Result<Response, ApiError> {
let uri = format!(
"{}public/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.collection_id,
p.display_id,
p.version
);
remove_owner(state, user, uri, &p.username).await
}
async fn add_owner(
state: AppState,
user: Option<sbol_db_core::User>,
object_uri: String,
headers: &HeaderMap,
body: &[u8],
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let form = parse_body::<AddOwnerForm>(headers, body)?;
let identifier = form
.user
.filter(|s| !s.is_empty())
.ok_or_else(|| ApiError::BadRequest("user is required".to_owned()))?;
let key = match identifier.rsplit_once('/') {
Some((_, segment)) => segment,
None => "",
};
let target_graph = state
.app
.users
.find_by_email_or_username(key)
.await?
.map(|u| u.graph_uri)
.ok_or_else(|| ApiError::BadRequest(format!("user {key} not recognized")))?;
service(&state)
.add_owner(&user.graph_uri, user.is_admin, &object_uri, &target_graph)
.await?;
Ok(success())
}
async fn remove_owner(
state: AppState,
user: Option<sbol_db_core::User>,
object_uri: String,
username: &str,
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let target_graph = resolve_target_graph(&state, username).await?;
service(&state)
.remove_owner(&user.graph_uri, user.is_admin, &object_uri, &target_graph)
.await?;
Ok(success())
}
async fn resolve_target_graph(state: &AppState, identifier: &str) -> Result<String, ApiError> {
let key = identifier.rsplit('/').next().unwrap_or(identifier);
let user = state
.app
.users
.find_by_email_or_username(key)
.await?
.ok_or_else(|| ApiError::NotFound(format!("user {key} not recognized")))?;
Ok(user.graph_uri)
}
fn parse_body<T: for<'de> Deserialize<'de> + Default>(
headers: &HeaderMap,
body: &[u8],
) -> Result<T, ApiError> {
if body.is_empty() {
return Ok(T::default());
}
let is_json = headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|v| v.starts_with("application/json"))
.unwrap_or(false);
if is_json {
serde_json::from_slice(body)
.map_err(|e| ApiError::BadRequest(format!("invalid JSON body: {e}")))
} else {
serde_urlencoded::from_bytes(body)
.map_err(|e| ApiError::BadRequest(format!("invalid form body: {e}")))
}
}