Initial commit
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
218
src/routes/users.rs
Normal file
218
src/routes/users.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
Json, Router, extract,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing,
|
||||
};
|
||||
use log::error;
|
||||
|
||||
use non_empty_string::NonEmptyString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
models::authelia, 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>>,
|
||||
}
|
||||
|
||||
impl From<(NonEmptyString, authelia::User)> for UserResponse {
|
||||
fn from((username, user): (NonEmptyString, authelia::User)) -> Self {
|
||||
Self {
|
||||
username,
|
||||
displayname: user.displayname,
|
||||
email: user.email,
|
||||
groups: Some(user.groups),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type UsersResponse = HashMap<NonEmptyString, UserResponse>;
|
||||
|
||||
impl From<authelia::Users> for UsersResponse {
|
||||
fn from(users: authelia::Users) -> Self {
|
||||
users
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|(key, user)| (key.clone(), UserResponse::from((key, user))))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all(
|
||||
_user: auth::User,
|
||||
extract::State(state): extract::State<State>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let users = state.load_users().map_err(|e| {
|
||||
error!("Failed to read users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(UsersResponse::from(users)))
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
_user: auth::User,
|
||||
extract::Path(username): extract::Path<NonEmptyString>,
|
||||
extract::State(state): extract::State<State>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let users = state.load_users().map_err(|e| {
|
||||
error!("Failed to read users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
users.get(&username).cloned().map_or_else(
|
||||
|| Err(StatusCode::NOT_FOUND),
|
||||
|user| Ok(Json(UserResponse::from((username, user))).into_response()),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserCreate {
|
||||
username: NonEmptyString,
|
||||
displayname: NonEmptyString,
|
||||
email: NonEmptyString,
|
||||
disabled: Option<bool>,
|
||||
groups: Option<Vec<NonEmptyString>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<UserCreate> for authelia::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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
_user: auth::User,
|
||||
extract::State(state): extract::State<State>,
|
||||
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) {
|
||||
return Err(StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
let user_created = authelia::User::from(user_create);
|
||||
users.users.insert(username.clone(), user_created.clone());
|
||||
|
||||
state.save_users(users).map_err(|e| {
|
||||
error!("Failed to save users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(UserResponse::from((username, user_created))).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserUpdate {
|
||||
username: Option<NonEmptyString>,
|
||||
displayname: NonEmptyString,
|
||||
email: NonEmptyString,
|
||||
disabled: Option<bool>,
|
||||
groups: Option<Vec<NonEmptyString>>,
|
||||
}
|
||||
|
||||
impl From<(Self, UserUpdate)> for authelia::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>,
|
||||
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 new_username = user_update
|
||||
.username
|
||||
.clone()
|
||||
.unwrap_or_else(|| username.clone());
|
||||
|
||||
let user_existing = users.remove(&username).ok_or(StatusCode::NOT_FOUND)?;
|
||||
let user_updated = authelia::User::from((user_existing, user_update));
|
||||
|
||||
users
|
||||
.users
|
||||
.insert(new_username.clone(), user_updated.clone());
|
||||
|
||||
state.save_users(users).map_err(|e| {
|
||||
error!("Failed to save users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
if user.username.to_string() == username && (username != new_username || user_updated.disabled)
|
||||
{
|
||||
return Ok(Redirect::to("/api/auth/logout").into_response());
|
||||
}
|
||||
|
||||
Ok(Json(UserResponse::from((new_username, user_updated))).into_response())
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
user: auth::User,
|
||||
extract::Path(username): extract::Path<String>,
|
||||
extract::State(state): extract::State<State>,
|
||||
) -> 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);
|
||||
}
|
||||
|
||||
state.save_users(users).map_err(|e| {
|
||||
error!("Failed to save users file: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
if user.username.to_string() == username {
|
||||
return Ok(Redirect::to("/api/auth/logout").into_response());
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT.into_response())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
Reference in New Issue
Block a user