use axum::body::Bytes;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use sbol_db_app::Registration;
use sbol_db_core::User;
use serde::Deserialize;
use serde_json::json;
use super::parse_body;
use crate::error::ApiError;
use crate::AppState;
#[derive(Deserialize, Default)]
struct CreateUserBody {
#[serde(default)]
username: Option<String>,
#[serde(default)]
name: Option<String>,
#[serde(default)]
email: Option<String>,
#[serde(default)]
affiliation: Option<String>,
#[serde(default)]
password: Option<String>,
#[serde(default, alias = "isAdmin")]
is_admin: Option<bool>,
#[serde(default, alias = "isCurator")]
is_curator: Option<bool>,
#[serde(default, alias = "isMember")]
is_member: Option<bool>,
}
pub async fn create_user(
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let form: CreateUserBody = parse_body(&headers, &body)?;
let username = required(form.username, "username")?;
let name = required(form.name, "name")?;
let email = required(form.email, "email")?;
let password = required(form.password, "password")?;
if state
.app
.users
.find_by_email_or_username(&username)
.await?
.is_some()
|| state
.app
.users
.find_by_email_or_username(&email)
.await?
.is_some()
{
return Err(ApiError::BadRequest(
"username or email already exists".to_owned(),
));
}
let registration = Registration {
username,
name,
email,
affiliation: form.affiliation.filter(|a| !a.is_empty()),
password,
is_admin: form.is_admin.unwrap_or(false),
is_curator: form.is_curator.unwrap_or(false),
is_member: form.is_member.unwrap_or(true),
};
let user = state.app.auth.register(registration).await?;
Ok((StatusCode::CREATED, Json(profile_json(&user))).into_response())
}
#[derive(Deserialize, Default)]
struct UpdateUserBody {
#[serde(default)]
username: Option<String>,
#[serde(default)]
email: Option<String>,
#[serde(default)]
name: Option<String>,
#[serde(default)]
affiliation: Option<String>,
#[serde(default, alias = "isAdmin")]
is_admin: Option<bool>,
#[serde(default, alias = "isCurator")]
is_curator: Option<bool>,
#[serde(default, alias = "isMember")]
is_member: Option<bool>,
}
pub async fn update_user(
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let form: UpdateUserBody = parse_body(&headers, &body)?;
let identifier = required(
form.username.clone().or_else(|| form.email.clone()),
"username",
)?;
let mut user = locate(&state, &identifier).await?;
if let Some(name) = form.name.filter(|n| !n.is_empty()) {
user.name = name;
}
if let Some(affiliation) = form.affiliation {
user.affiliation = Some(affiliation).filter(|a| !a.is_empty());
}
if let Some(is_admin) = form.is_admin {
user.is_admin = is_admin;
}
if let Some(is_curator) = form.is_curator {
user.is_curator = is_curator;
}
if let Some(is_member) = form.is_member {
user.is_member = is_member;
}
let updated = state.app.users.update_user(&user).await?;
Ok(Json(profile_json(&updated)).into_response())
}
#[derive(Deserialize, Default)]
struct DeleteUserBody {
#[serde(default)]
username: Option<String>,
#[serde(default)]
email: Option<String>,
}
pub async fn delete_user(
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let form: DeleteUserBody = parse_body(&headers, &body)?;
let identifier = required(form.username.or(form.email), "username")?;
let user = locate(&state, &identifier).await?;
if state.app.users.delete_user(user.id).await? {
Ok(Json(json!({ "status": "deleted", "username": user.username })).into_response())
} else {
Err(ApiError::NotFound(format!("user {identifier}")))
}
}
async fn locate(state: &AppState, identifier: &str) -> Result<User, ApiError> {
state
.app
.users
.find_by_email_or_username(identifier)
.await?
.ok_or_else(|| ApiError::NotFound(format!("user {identifier}")))
}
fn profile_json(user: &User) -> serde_json::Value {
json!({
"username": user.username,
"name": user.name,
"email": user.email,
"affiliation": user.affiliation,
"graphUri": user.graph_uri,
"isAdmin": user.is_admin,
"isCurator": user.is_curator,
"isMember": user.is_member,
})
}
fn required(value: Option<String>, field: &str) -> Result<String, ApiError> {
match value {
Some(v) if !v.trim().is_empty() => Ok(v),
_ => Err(ApiError::BadRequest(format!("{field} is required"))),
}
}