use axum::body::Bytes;
use axum::extract::{Query, State};
use axum::http::header::{CACHE_CONTROL, PRAGMA};
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::{Extension, Json};
use chrono::{DateTime, Utc};
use sbol_db_app::ProfileUpdate;
use sbol_db_core::{DomainError, User, UserId};
use serde::{Deserialize, Serialize};
use super::auth::{require_user, Identity};
use super::error::V2Error;
use super::util::{parse_json, required};
use crate::error::ApiError;
use crate::AppState;
#[derive(Debug, Serialize)]
pub(super) struct AccountResponse {
id: UserId,
username: String,
name: String,
email: String,
affiliation: Option<String>,
graph_uri: String,
is_admin: bool,
is_curator: bool,
is_member: bool,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl From<User> for AccountResponse {
fn from(user: User) -> Self {
Self {
id: user.id,
username: user.username,
name: user.name,
email: user.email,
affiliation: user.affiliation,
graph_uri: user.graph_uri,
is_admin: user.is_admin,
is_curator: user.is_curator,
is_member: user.is_member,
created_at: user.created_at,
updated_at: user.updated_at,
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct AccountPatch {
name: Option<String>,
affiliation: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct PasswordChange {
current_password: Option<String>,
new_password: Option<String>,
}
#[derive(Debug, Serialize)]
struct SharedObjectsResponse {
items: Vec<sbol_db_app::ObjectDetails>,
total: usize,
offset: usize,
limit: usize,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub(super) struct SharedPaging {
offset: Option<usize>,
limit: Option<usize>,
}
pub(super) async fn get(Extension(identity): Extension<Identity>) -> Result<Response, V2Error> {
let user = require_user(&identity)?;
Ok(no_store(Json(AccountResponse::from(user)).into_response()))
}
pub(super) async fn patch(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
body: Bytes,
) -> Result<Response, V2Error> {
let user = require_user(&identity)?;
let patch: AccountPatch = parse_json(&body)?;
let updated = state
.app
.auth
.update_profile(
&user,
ProfileUpdate {
name: patch.name,
affiliation: patch.affiliation,
},
)
.await?;
Ok(no_store(
Json(AccountResponse::from(updated)).into_response(),
))
}
pub(super) async fn change_password(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
body: Bytes,
) -> Result<Response, V2Error> {
let user = require_user(&identity)?;
let request: PasswordChange = parse_json(&body)?;
let current = required(request.current_password, "current_password")?;
let new = required(request.new_password, "new_password")?;
match state
.app
.auth
.change_password(&user, ¤t, &new, &state.config.password_salt)
.await
{
Ok(()) => Ok(no_store(StatusCode::NO_CONTENT.into_response())),
Err(DomainError::Validation(_)) => Err(V2Error::from(ApiError::BadRequest(
"current password is incorrect".to_owned(),
))),
Err(error) => Err(error.into()),
}
}
pub(super) async fn shared(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Query(paging): Query<SharedPaging>,
) -> Result<Response, V2Error> {
let user = require_user(&identity)?;
let offset = paging.offset.unwrap_or(0);
let limit = paging.limit.unwrap_or(24).clamp(1, 100);
let (items, total) = state
.app
.shared_object_details_page(&user.graph_uri, offset, limit)
.await?;
Ok(no_store(
Json(SharedObjectsResponse {
items,
total,
offset,
limit,
})
.into_response(),
))
}
fn no_store(mut response: Response) -> Response {
let headers: &mut HeaderMap = response.headers_mut();
headers.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
headers.insert(PRAGMA, HeaderValue::from_static("no-cache"));
response
}