use std::collections::HashMap; use axum::{ Json, Router, extract, http::StatusCode, response::{IntoResponse, Redirect}, routing, }; use non_empty_string::NonEmptyString; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use crate::{ config::Config, models, routes::auth, state::State, utils::crypto::generate_random_password_hash, }; #[derive(Debug, Serialize)] struct UserResponse { display_name: String, email: String, disabled: bool, image: Option, groups: Vec, } impl From for UserResponse { fn from(user: models::users::UserWithGroups) -> Self { Self { display_name: user.display_name, email: user.email, disabled: user.disabled, image: user.image, groups: user.groups, } } } type UsersResponse = HashMap; pub async fn get_all( _: auth::User, extract::State(pg_pool): extract::State, ) -> Result { let users_with_groups = models::users::UserWithGroups::select(&pg_pool) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; let users_response = users_with_groups .into_iter() .map(|user| (user.name.clone(), UserResponse::from(user))) .collect::(); Ok(Json(users_response)) } pub async fn get( _: auth::User, extract::Path(name): extract::Path, extract::State(pg_pool): extract::State, ) -> Result { 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)?; Ok(Json(UserResponse::from(user_with_groups))) } #[derive(Debug, Deserialize)] pub struct UserCreate { name: NonEmptyString, displayname: NonEmptyString, email: NonEmptyString, disabled: bool, image: Option, groups: Vec, } pub async fn create( _: auth::User, extract::State(pg_pool): extract::State, extract::Json(user_create): extract::Json, ) -> Result { 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 groups = user_create .groups .into_iter() .map(|g| g.to_string()) .collect::>(); if !models::groups::Group::all_exist_by_names(&pg_pool, &groups) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? { return Err(StatusCode::NOT_FOUND); } 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 { display_name: Option, email: Option, disabled: Option, image: Option, groups: Option>, } pub async fn update( session_user: auth::User, extract::Path(name): extract::Path, extract::State(pg_pool): extract::State, extract::State(config): extract::State, extract::Json(user_update): extract::Json, ) -> Result { 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 mut logout = false; if let Some(groups) = user_update.groups { let groups = groups .into_iter() .map(|g| g.to_string()) .collect::>(); if !models::groups::Group::all_exist_by_names(&pg_pool, &groups) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? { return Err(StatusCode::NOT_FOUND); } models::intersections::UsersGroups::set_groups_for_user( &pg_pool, user.name.as_str(), &groups, ) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; 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(().into_response()) } pub async fn delete( session_user: auth::User, extract::Path(name): extract::Path, extract::State(pg_pool): extract::State, ) -> Result { if name == session_user.username.to_string() { return Err(StatusCode::FORBIDDEN); } let user = models::users::User::select_by_name(&pg_pool, &name) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? .ok_or(StatusCode::NOT_FOUND)?; models::users::User::delete_by_name(&pg_pool, &user.name) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; Ok(()) } pub fn routes(state: State) -> Router { Router::new() .route("/users", routing::get(get_all)) .route("/users/{username}", routing::get(get)) .route("/users", routing::post(create)) .route("/users/{username}", routing::put(update)) .route("/users/{username}", routing::delete(delete)) .with_state(state) }