Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-06-05 14:24:48 +01:00
parent 050f25bba9
commit ed958a8ed0
41 changed files with 1885 additions and 480 deletions

View File

@@ -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 {