This repository has been archived on 2025-07-31. You can view files and clone it, but cannot push or open issues or pull requests.
Files
glyph/src/routes/users.rs
Nikolaos Karaolidis ed958a8ed0 Add sqlx
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-06-05 14:24:48 +01:00

230 lines
6.5 KiB
Rust

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<String>,
groups: Vec<String>,
}
impl From<models::users::UserWithGroups> 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<String, UserResponse>;
pub async fn get_all(
_: auth::User,
extract::State(pg_pool): extract::State<PgPool>,
) -> Result<impl IntoResponse, StatusCode> {
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::<UsersResponse>();
Ok(Json(users_response))
}
pub async fn get(
_: auth::User,
extract::Path(name): extract::Path<NonEmptyString>,
extract::State(pg_pool): extract::State<PgPool>,
) -> Result<impl IntoResponse, StatusCode> {
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<NonEmptyString>,
groups: Vec<NonEmptyString>,
}
pub async fn create(
_: auth::User,
extract::State(pg_pool): extract::State<PgPool>,
extract::Json(user_create): extract::Json<UserCreate>,
) -> Result<impl IntoResponse, StatusCode> {
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::<Vec<_>>();
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<NonEmptyString>,
email: Option<NonEmptyString>,
disabled: Option<bool>,
image: Option<NonEmptyString>,
groups: Option<Vec<NonEmptyString>>,
}
pub async fn update(
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 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::<Vec<_>>();
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<String>,
extract::State(pg_pool): extract::State<PgPool>,
) -> Result<impl IntoResponse, StatusCode> {
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)
}