Initial commit

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-06-04 22:50:18 +01:00
commit ec7055d5ff
22 changed files with 5558 additions and 0 deletions

218
src/routes/users.rs Normal file
View 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)
}