@@ -6,205 +6,216 @@ use axum::{
|
||||
response::{IntoResponse, Redirect},
|
||||
routing,
|
||||
};
|
||||
use log::error;
|
||||
|
||||
use non_empty_string::NonEmptyString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::users, routes::auth, state::State, utils::crypto::generate_random_password_hash,
|
||||
config::Config, models, routes::auth, state::State,
|
||||
utils::crypto::generate_random_password_hash,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct UserResponse {
|
||||
username: NonEmptyString,
|
||||
displayname: NonEmptyString,
|
||||
email: Option<String>,
|
||||
groups: Option<Vec<NonEmptyString>>,
|
||||
display_name: String,
|
||||
email: String,
|
||||
disabled: bool,
|
||||
image: Option<String>,
|
||||
groups: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<(NonEmptyString, users::User)> for UserResponse {
|
||||
fn from((username, user): (NonEmptyString, users::User)) -> Self {
|
||||
impl From<models::users::UserWithGroups> for UserResponse {
|
||||
fn from(user: models::users::UserWithGroups) -> Self {
|
||||
Self {
|
||||
username,
|
||||
displayname: user.displayname,
|
||||
display_name: user.display_name,
|
||||
email: user.email,
|
||||
groups: Some(user.groups),
|
||||
disabled: user.disabled,
|
||||
image: user.image,
|
||||
groups: user.groups,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type UsersResponse = HashMap<NonEmptyString, UserResponse>;
|
||||
|
||||
impl From<users::Users> for UsersResponse {
|
||||
fn from(users: users::Users) -> Self {
|
||||
users
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|(key, user)| (key.clone(), UserResponse::from((key, user))))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
type UsersResponse = HashMap<String, UserResponse>;
|
||||
|
||||
pub async fn get_all(
|
||||
_user: auth::User,
|
||||
extract::State(state): extract::State<State>,
|
||||
_: auth::User,
|
||||
extract::State(pg_pool): extract::State<PgPool>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let users = state.load_users().map_err(|e| {
|
||||
error!("Failed to read users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
let users_with_groups = models::users::UserWithGroups::select(&pg_pool)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||
|
||||
Ok(Json(UsersResponse::from(users)))
|
||||
let users_response = users_with_groups
|
||||
.into_iter()
|
||||
.map(|user| (user.name.clone(), UserResponse::from(user)))
|
||||
.collect::<UsersResponse>();
|
||||
|
||||
Ok(Json(users_response))
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
_user: auth::User,
|
||||
extract::Path(username): extract::Path<NonEmptyString>,
|
||||
extract::State(state): extract::State<State>,
|
||||
_: auth::User,
|
||||
extract::Path(name): extract::Path<NonEmptyString>,
|
||||
extract::State(pg_pool): extract::State<PgPool>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let users = state.load_users().map_err(|e| {
|
||||
error!("Failed to read users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
let user_with_groups = models::users::UserWithGroups::select_by_name(&pg_pool, name.as_str())
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
users.get(&username).cloned().map_or_else(
|
||||
|| Err(StatusCode::NOT_FOUND),
|
||||
|user| Ok(Json(UserResponse::from((username, user))).into_response()),
|
||||
)
|
||||
Ok(Json(UserResponse::from(user_with_groups)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserCreate {
|
||||
username: NonEmptyString,
|
||||
name: NonEmptyString,
|
||||
displayname: NonEmptyString,
|
||||
email: NonEmptyString,
|
||||
disabled: Option<bool>,
|
||||
groups: Option<Vec<NonEmptyString>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<UserCreate> for users::User {
|
||||
fn from(user_create: UserCreate) -> Self {
|
||||
Self {
|
||||
displayname: user_create.displayname,
|
||||
email: Some(user_create.email.to_string()),
|
||||
password: NonEmptyString::new(generate_random_password_hash()).unwrap(),
|
||||
disabled: user_create.disabled.unwrap_or(false),
|
||||
groups: user_create.groups.unwrap_or_default(),
|
||||
extra: None,
|
||||
}
|
||||
}
|
||||
disabled: bool,
|
||||
image: Option<NonEmptyString>,
|
||||
groups: Vec<NonEmptyString>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
_user: auth::User,
|
||||
extract::State(state): extract::State<State>,
|
||||
_: auth::User,
|
||||
extract::State(pg_pool): extract::State<PgPool>,
|
||||
extract::Json(user_create): extract::Json<UserCreate>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let mut users = state.load_users().map_err(|e| {
|
||||
error!("Failed to read users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let username = user_create.username.clone();
|
||||
if users.contains_key(&username) {
|
||||
if models::users::User::select_by_name(&pg_pool, user_create.name.as_str())
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
|
||||
.is_some()
|
||||
{
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
let user_created = users::User::from(user_create);
|
||||
users.users.insert(username.clone(), user_created.clone());
|
||||
let groups = user_create
|
||||
.groups
|
||||
.into_iter()
|
||||
.map(|g| g.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
state.save_users(users).map_err(|e| {
|
||||
error!("Failed to save users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
if !models::groups::Group::all_exist_by_names(&pg_pool, &groups)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
|
||||
{
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
Ok(Json(UserResponse::from((username, user_created))).into_response())
|
||||
let user_with_groups = models::users::UserWithGroups {
|
||||
name: user_create.name.to_string(),
|
||||
display_name: user_create.displayname.to_string(),
|
||||
password: generate_random_password_hash(),
|
||||
email: user_create.email.to_string(),
|
||||
disabled: user_create.disabled,
|
||||
image: user_create.image.map(|i| i.to_string()),
|
||||
groups,
|
||||
};
|
||||
|
||||
models::users::UserWithGroups::insert(&pg_pool, &user_with_groups)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserUpdate {
|
||||
username: Option<NonEmptyString>,
|
||||
displayname: NonEmptyString,
|
||||
email: NonEmptyString,
|
||||
display_name: Option<NonEmptyString>,
|
||||
email: Option<NonEmptyString>,
|
||||
disabled: Option<bool>,
|
||||
image: Option<NonEmptyString>,
|
||||
groups: Option<Vec<NonEmptyString>>,
|
||||
}
|
||||
|
||||
impl From<(Self, UserUpdate)> for users::User {
|
||||
fn from((user_existing, user_update): (Self, UserUpdate)) -> Self {
|
||||
Self {
|
||||
displayname: user_update.displayname,
|
||||
email: Some(user_update.email.to_string()),
|
||||
password: user_existing.password,
|
||||
disabled: user_update.disabled.unwrap_or(user_existing.disabled),
|
||||
groups: user_update.groups.unwrap_or(user_existing.groups),
|
||||
extra: user_existing.extra,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
user: auth::User,
|
||||
extract::Path(username): extract::Path<NonEmptyString>,
|
||||
extract::State(state): extract::State<State>,
|
||||
session_user: auth::User,
|
||||
extract::Path(name): extract::Path<NonEmptyString>,
|
||||
extract::State(pg_pool): extract::State<PgPool>,
|
||||
extract::State(config): extract::State<Config>,
|
||||
extract::Json(user_update): extract::Json<UserUpdate>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let mut users = state.load_users().map_err(|e| {
|
||||
error!("Failed to read users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
let user = models::users::User::select_by_name(&pg_pool, name.as_str())
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
let new_username = user_update
|
||||
.username
|
||||
.clone()
|
||||
.unwrap_or_else(|| username.clone());
|
||||
let mut logout = false;
|
||||
|
||||
let user_existing = users.remove(&username).ok_or(StatusCode::NOT_FOUND)?;
|
||||
let user_updated = users::User::from((user_existing, user_update));
|
||||
if let Some(groups) = user_update.groups {
|
||||
let groups = groups
|
||||
.into_iter()
|
||||
.map(|g| g.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
users
|
||||
.users
|
||||
.insert(new_username.clone(), user_updated.clone());
|
||||
if !models::groups::Group::all_exist_by_names(&pg_pool, &groups)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
|
||||
{
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
state.save_users(users).map_err(|e| {
|
||||
error!("Failed to save users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
models::intersections::UsersGroups::set_groups_for_user(
|
||||
&pg_pool,
|
||||
user.name.as_str(),
|
||||
&groups,
|
||||
)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||
|
||||
if user.username.to_string() == username && (username != new_username || user_updated.disabled)
|
||||
{
|
||||
if name == session_user.username.to_string() && !groups.contains(&config.oauth.admin_group)
|
||||
{
|
||||
logout = true;
|
||||
}
|
||||
}
|
||||
|
||||
let user = models::users::User {
|
||||
name: user.name,
|
||||
display_name: user_update
|
||||
.display_name
|
||||
.map(|d| d.to_string())
|
||||
.unwrap_or(user.display_name),
|
||||
password: user.password,
|
||||
email: user_update
|
||||
.email
|
||||
.map(|e| e.to_string())
|
||||
.unwrap_or(user.email),
|
||||
disabled: user_update.disabled.unwrap_or(user.disabled),
|
||||
image: user_update.image.map(|i| i.to_string()).or(user.image),
|
||||
};
|
||||
|
||||
models::users::User::upsert(&pg_pool, &user)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||
|
||||
if logout {
|
||||
return Ok(Redirect::to("/api/auth/logout").into_response());
|
||||
}
|
||||
|
||||
Ok(Json(UserResponse::from((new_username, user_updated))).into_response())
|
||||
Ok(().into_response())
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
user: auth::User,
|
||||
extract::Path(username): extract::Path<String>,
|
||||
extract::State(state): extract::State<State>,
|
||||
session_user: auth::User,
|
||||
extract::Path(name): extract::Path<String>,
|
||||
extract::State(pg_pool): extract::State<PgPool>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let mut users = state.load_users().map_err(|e| {
|
||||
error!("Failed to read users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
if users.remove(&username).is_none() {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
if name == session_user.username.to_string() {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
state.save_users(users).map_err(|e| {
|
||||
error!("Failed to save users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
let user = models::users::User::select_by_name(&pg_pool, &name)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
if user.username.to_string() == username {
|
||||
return Ok(Redirect::to("/api/auth/logout").into_response());
|
||||
}
|
||||
models::users::User::delete_by_name(&pg_pool, &user.name)
|
||||
.await
|
||||
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT.into_response())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn routes(state: State) -> Router {
|
||||
|
Reference in New Issue
Block a user