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,227 +6,173 @@ use axum::{
response::{IntoResponse, Redirect},
routing,
};
use log::error;
use non_empty_string::NonEmptyString;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use crate::{models::groups, routes::auth, state::State};
use crate::{
config::Config,
models::{self, groups::Group},
routes::auth,
state::State,
};
#[derive(Debug, Serialize)]
struct GroupResponse {
groupname: NonEmptyString,
users: Vec<NonEmptyString>,
users: Vec<String>,
}
impl From<(NonEmptyString, groups::Group)> for GroupResponse {
fn from((groupname, group): (NonEmptyString, groups::Group)) -> Self {
Self {
groupname,
users: group.users,
}
impl From<models::groups::GroupWithUsers> for GroupResponse {
fn from(group: models::groups::GroupWithUsers) -> Self {
Self { users: group.users }
}
}
type GroupsResponse = HashMap<NonEmptyString, GroupResponse>;
impl From<groups::Groups> for GroupsResponse {
fn from(groups: groups::Groups) -> Self {
groups
.groups
.into_iter()
.map(|(key, group)| (key.clone(), GroupResponse::from((key, group))))
.collect()
}
}
type GroupsResponse = HashMap<String, GroupResponse>;
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 groups = state.load_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let groups_with_users = models::groups::GroupWithUsers::select(&pg_pool)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
Ok(Json(GroupsResponse::from(groups)))
let groups_response = groups_with_users
.into_iter()
.map(|group| (group.name.clone(), GroupResponse::from(group)))
.collect::<GroupsResponse>();
Ok(Json(groups_response))
}
pub async fn get(
_user: auth::User,
extract::Path(groupname): 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 groups = state.load_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let group_with_users = models::groups::GroupWithUsers::select_by_name(&pg_pool, name.as_str())
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.ok_or(StatusCode::NOT_FOUND)?;
groups.get(&groupname).cloned().map_or_else(
|| Err(StatusCode::NOT_FOUND),
|group| Ok(Json(GroupResponse::from((groupname, group))).into_response()),
)
Ok(Json(GroupResponse::from(group_with_users)))
}
#[derive(Debug, Deserialize)]
pub struct GroupCreate {
groupname: NonEmptyString,
name: NonEmptyString,
users: Vec<NonEmptyString>,
}
impl From<GroupCreate> for groups::Group {
fn from(update: GroupCreate) -> Self {
Self {
users: update.users,
}
}
}
pub async fn create(
_user: auth::User,
extract::State(state): extract::State<State>,
_: auth::User,
extract::State(pg_pool): extract::State<PgPool>,
extract::Json(group_create): extract::Json<GroupCreate>,
) -> Result<impl IntoResponse, StatusCode> {
let (mut users, groups) = state.load_users_and_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let groupname = group_create.groupname.clone();
if groups.contains_key(&groupname) {
if models::groups::Group::select_by_name(&pg_pool, group_create.name.as_str())
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.is_some()
{
return Err(StatusCode::CONFLICT);
}
let group_created = groups::Group::from(group_create);
let users = group_create
.users
.into_iter()
.map(|u| u.to_string())
.collect::<Vec<_>>();
for username in &group_created.users {
if !users.contains_key(username) {
return Err(StatusCode::NOT_FOUND);
}
users
.get_mut(username)
.unwrap()
.groups
.push(groupname.clone());
if !models::users::User::all_exist_by_names(&pg_pool, &users)
.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
})?;
let group_with_users = models::groups::GroupWithUsers {
name: group_create.name.to_string(),
users,
};
Ok(Json(GroupResponse::from((groupname, group_created))).into_response())
models::groups::GroupWithUsers::insert(&pg_pool, &group_with_users)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
Ok(())
}
#[derive(Debug, Deserialize)]
pub struct GroupUpdate {
groupname: Option<NonEmptyString>,
users: Vec<NonEmptyString>,
}
impl From<GroupUpdate> for groups::Group {
fn from(update: GroupUpdate) -> Self {
Self {
users: update.users,
}
}
users: Option<Vec<NonEmptyString>>,
}
pub async fn update(
user: auth::User,
extract::Path(groupname): 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(group_update): extract::Json<GroupUpdate>,
) -> Result<impl IntoResponse, StatusCode> {
let (mut users, groups) = state.load_users_and_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let group = models::groups::Group::select_by_name(&pg_pool, name.as_str())
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.ok_or(StatusCode::NOT_FOUND)?;
let new_groupname = group_update
.groupname
.clone()
.unwrap_or_else(|| groupname.clone());
let mut logout = false;
let group_existing = groups.get(&groupname).ok_or(StatusCode::NOT_FOUND)?;
let group_updated = groups::Group::from(group_update);
if let Some(users) = &group_update.users {
let users = users.iter().map(ToString::to_string).collect::<Vec<_>>();
if groupname != new_groupname
&& (groupname == state.config.oauth.admin_group
|| new_groupname == state.config.oauth.admin_group)
{
return Err(StatusCode::FORBIDDEN);
}
if groupname != new_groupname && groups.contains_key(&new_groupname) {
return Err(StatusCode::CONFLICT);
}
for user in &group_existing.users {
let user = users.get_mut(user).unwrap();
let pos = user.groups.iter().position(|g| g == &groupname).unwrap();
user.groups.remove(pos);
}
for username in &group_updated.users {
if !users.contains_key(username) {
if !models::users::User::all_exist_by_names(&pg_pool, &users)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
{
return Err(StatusCode::NOT_FOUND);
}
let user = users.get_mut(username).unwrap();
if !user.groups.contains(&new_groupname) {
user.groups.push(new_groupname.clone());
models::intersections::UsersGroups::set_users_for_group(
&pg_pool,
group.name.as_str(),
&users,
)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
if name == config.oauth.admin_group && !users.contains(&session_user.username) {
logout = true;
}
}
state.save_users(users).map_err(|e| {
error!("Failed to save users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if new_groupname == state.config.oauth.admin_group
&& !group_updated
.users
.iter()
.any(|group_user| *group_user == *user.username.to_string())
{
if logout {
return Ok(Redirect::to("/api/auth/logout").into_response());
}
Ok(Json(GroupResponse::from((new_groupname, group_updated))).into_response())
Ok(().into_response())
}
pub async fn delete(
_user: auth::User,
extract::Path(groupname): extract::Path<String>,
extract::State(state): extract::State<State>,
_: auth::User,
extract::Path(name): extract::Path<String>,
extract::State(pg_pool): extract::State<PgPool>,
extract::State(config): extract::State<Config>,
) -> Result<impl IntoResponse, StatusCode> {
let (mut users, groups) = state.load_users_and_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if groupname == state.config.oauth.admin_group {
if name == config.oauth.admin_group {
return Err(StatusCode::FORBIDDEN);
}
if let Some(old_group) = groups.get(&groupname) {
for user in &old_group.users {
let user = users.get_mut(user).unwrap();
let pos = user.groups.iter().position(|g| g == &groupname).unwrap();
user.groups.remove(pos);
}
} else {
return Err(StatusCode::NOT_FOUND);
}
let group = models::groups::Group::select_by_name(&pg_pool, &name)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.ok_or(StatusCode::NOT_FOUND)?;
state.save_users(users).map_err(|e| {
error!("Failed to save users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Group::delete_by_name(&pg_pool, &group.name)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
Ok(StatusCode::NO_CONTENT.into_response())
Ok(())
}
pub fn routes(state: State) -> Router {