@@ -41,6 +41,15 @@ pub struct AutheliaConfig {
|
||||
pub user_database: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PostgresqlConfig {
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub database: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct RedisConfig {
|
||||
pub host: String,
|
||||
@@ -54,6 +63,7 @@ pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
pub oauth: OAuthConfig,
|
||||
pub authelia: AutheliaConfig,
|
||||
pub postgresql: PostgresqlConfig,
|
||||
pub redis: RedisConfig,
|
||||
}
|
||||
|
||||
|
13
src/main.rs
13
src/main.rs
@@ -11,7 +11,7 @@ use axum::serve;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use log4rs::config::Deserializers;
|
||||
use std::net::SocketAddr;
|
||||
use std::{error::Error, net::SocketAddr};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use config::{Args, Config};
|
||||
@@ -25,6 +25,8 @@ async fn main() {
|
||||
let config = Config::try_from(&args.config).unwrap();
|
||||
let state = State::from_config(config.clone()).await;
|
||||
|
||||
init(&state).await.unwrap();
|
||||
|
||||
let routes = routes::routes(state);
|
||||
let app = axum::Router::new().nest(&format!("{}/api", config.server.subpath), routes);
|
||||
|
||||
@@ -34,3 +36,12 @@ async fn main() {
|
||||
info!("Listening on {}", listener.local_addr().unwrap());
|
||||
serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn init(state: &State) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&state.pg_pool)
|
||||
.await
|
||||
.expect("Failed to run migrations");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
use non_empty_string::NonEmptyString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -6,50 +5,20 @@ use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UsersFile {
|
||||
pub users: HashMap<NonEmptyString, UserFile>,
|
||||
pub users: HashMap<String, UserFile>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: Option<HashMap<NonEmptyString, Value>>,
|
||||
pub extra: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserFile {
|
||||
pub displayname: NonEmptyString,
|
||||
pub password: NonEmptyString,
|
||||
pub displayname: String,
|
||||
pub password: String,
|
||||
pub email: Option<String>,
|
||||
pub disabled: Option<bool>,
|
||||
pub groups: Option<Vec<NonEmptyString>>,
|
||||
pub groups: Option<Vec<String>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: Option<HashMap<NonEmptyString, Value>>,
|
||||
}
|
||||
|
||||
impl From<super::users::User> for UserFile {
|
||||
fn from(user: super::users::User) -> Self {
|
||||
Self {
|
||||
displayname: user.displayname,
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
disabled: if user.disabled { Some(true) } else { None },
|
||||
groups: if user.groups.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(user.groups)
|
||||
},
|
||||
extra: user.extra,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<super::users::Users> for UsersFile {
|
||||
fn from(users: super::users::Users) -> Self {
|
||||
Self {
|
||||
users: users
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|(key, user)| (key, UserFile::from(user)))
|
||||
.collect(),
|
||||
extra: users.extra,
|
||||
}
|
||||
}
|
||||
pub extra: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
@@ -1,49 +1,143 @@
|
||||
use non_empty_string::NonEmptyString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgPool, prelude::FromRow, query, query_as};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Group {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub async fn select_by_name(
|
||||
pool: &PgPool,
|
||||
name: &str,
|
||||
) -> Result<Option<Self>, Box<dyn Error + Send + Sync>> {
|
||||
let group = query_as!(
|
||||
Group,
|
||||
r#"
|
||||
SELECT name
|
||||
FROM groups
|
||||
WHERE name = $1
|
||||
"#,
|
||||
name
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
pub async fn delete_by_name(
|
||||
pool: &PgPool,
|
||||
name: &str,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
query!(
|
||||
r#"
|
||||
DELETE FROM groups
|
||||
WHERE name = $1
|
||||
"#,
|
||||
name
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn all_exist_by_names(
|
||||
pool: &PgPool,
|
||||
names: &[String],
|
||||
) -> Result<bool, Box<dyn Error + Send + Sync>> {
|
||||
let row = query!(
|
||||
r#"
|
||||
SELECT COUNT(*) AS "count!"
|
||||
FROM groups
|
||||
WHERE name = ANY($1)
|
||||
"#,
|
||||
names
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.count == i64::try_from(names.len()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Group {
|
||||
pub users: Vec<NonEmptyString>,
|
||||
pub struct GroupWithUsers {
|
||||
pub name: String,
|
||||
pub users: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct Groups {
|
||||
pub groups: HashMap<NonEmptyString, Group>,
|
||||
}
|
||||
|
||||
impl Deref for Groups {
|
||||
type Target = HashMap<NonEmptyString, Group>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.groups
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Groups {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.groups
|
||||
}
|
||||
}
|
||||
|
||||
impl From<super::authelia::UsersFile> for Groups {
|
||||
fn from(users_file: super::authelia::UsersFile) -> Self {
|
||||
users_file.users.into_iter().fold(
|
||||
Self {
|
||||
groups: HashMap::new(),
|
||||
},
|
||||
|mut acc, (key, user)| {
|
||||
for group in user.groups.unwrap_or_default() {
|
||||
acc.entry(group)
|
||||
.or_insert_with(|| Group { users: Vec::new() })
|
||||
.users
|
||||
.push(key.clone());
|
||||
}
|
||||
acc
|
||||
},
|
||||
impl GroupWithUsers {
|
||||
pub async fn select(pool: &PgPool) -> Result<Vec<Self>, Box<dyn Error + Send + Sync>> {
|
||||
let groups = query_as!(
|
||||
GroupWithUsers,
|
||||
r#"
|
||||
SELECT
|
||||
g.name,
|
||||
COALESCE(array_agg(ug.user_name ORDER BY ug.user_name), ARRAY[]::TEXT[]) AS "users!"
|
||||
FROM groups g
|
||||
LEFT JOIN users_groups ug ON g.name = ug.group_name
|
||||
GROUP BY g.name
|
||||
"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
pub async fn select_by_name(
|
||||
pool: &PgPool,
|
||||
name: &str,
|
||||
) -> Result<Option<Self>, Box<dyn Error + Send + Sync>> {
|
||||
let group = query_as!(
|
||||
GroupWithUsers,
|
||||
r#"
|
||||
SELECT
|
||||
g.name,
|
||||
COALESCE(array_agg(ug.user_name ORDER BY ug.user_name), ARRAY[]::TEXT[]) AS "users!"
|
||||
FROM groups g
|
||||
LEFT JOIN users_groups ug ON g.name = ug.group_name
|
||||
WHERE g.name = $1
|
||||
GROUP BY g.name
|
||||
"#,
|
||||
name
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
pool: &PgPool,
|
||||
group_with_users: &Self,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
query!(
|
||||
r#"INSERT INTO groups (name) VALUES ($1)"#,
|
||||
group_with_users.name
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
query!(
|
||||
r#"
|
||||
INSERT INTO users_groups (user_name, group_name)
|
||||
SELECT * FROM UNNEST($1::text[], $2::text[])
|
||||
"#,
|
||||
&group_with_users.users,
|
||||
&vec![group_with_users.name.clone(); group_with_users.users.len()]
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
74
src/models/intersections.rs
Normal file
74
src/models/intersections.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::error::Error;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, PgPool, query};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct UsersGroups {
|
||||
pub user_name: String,
|
||||
pub group_name: String,
|
||||
}
|
||||
|
||||
impl UsersGroups {
|
||||
pub async fn set_users_for_group(
|
||||
pool: &PgPool,
|
||||
group_name: &str,
|
||||
users: &[String],
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
query!(
|
||||
r#"
|
||||
DELETE FROM users_groups
|
||||
WHERE group_name = $1
|
||||
"#,
|
||||
group_name
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
query!(
|
||||
r#"
|
||||
INSERT INTO users_groups (user_name, group_name)
|
||||
SELECT * FROM UNNEST($1::text[], $2::text[])
|
||||
"#,
|
||||
users,
|
||||
&vec![group_name.to_string(); users.len()]
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_groups_for_user(
|
||||
pool: &PgPool,
|
||||
user_name: &str,
|
||||
groups: &[String],
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
query!(
|
||||
r#"
|
||||
DELETE FROM users_groups
|
||||
WHERE user_name = $1
|
||||
"#,
|
||||
user_name
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
query!(
|
||||
r#"
|
||||
INSERT INTO users_groups (user_name, group_name)
|
||||
SELECT * FROM UNNEST($1::text[], $2::text[])
|
||||
"#,
|
||||
&vec![user_name.to_string(); groups.len()],
|
||||
groups
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
use redis_macros::{FromRedisValue, ToRedisArgs};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use time::UtcDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRedisValue, ToRedisArgs)]
|
||||
#[derive(Serialize, Deserialize, FromRow)]
|
||||
struct Invite {
|
||||
id: Uuid,
|
||||
groups: Vec<String>,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
pub mod authelia;
|
||||
pub mod groups;
|
||||
pub mod intersections;
|
||||
pub mod invites;
|
||||
pub mod users;
|
||||
|
@@ -1,68 +1,199 @@
|
||||
use non_empty_string::NonEmptyString;
|
||||
use std::error::Error;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sqlx::{FromRow, PgPool, query, query_as};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct User {
|
||||
pub displayname: NonEmptyString,
|
||||
pub email: Option<String>,
|
||||
pub password: NonEmptyString,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub password: String,
|
||||
pub email: String,
|
||||
#[serde(default)]
|
||||
pub disabled: bool,
|
||||
pub groups: Vec<NonEmptyString>,
|
||||
#[serde(default)]
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: Option<HashMap<NonEmptyString, Value>>,
|
||||
impl User {
|
||||
pub async fn select_by_name(
|
||||
pool: &PgPool,
|
||||
name: &str,
|
||||
) -> Result<Option<Self>, Box<dyn Error + Send + Sync>> {
|
||||
let user = query_as!(
|
||||
User,
|
||||
r#"
|
||||
SELECT name, display_name, password, email, disabled, image
|
||||
FROM users
|
||||
WHERE name = $1
|
||||
"#,
|
||||
name
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn upsert(pool: &PgPool, user: &Self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
query!(
|
||||
r#"
|
||||
INSERT INTO users (name, display_name, password, email, disabled, image)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (name) DO UPDATE
|
||||
SET display_name = EXCLUDED.display_name,
|
||||
password = EXCLUDED.password,
|
||||
email = EXCLUDED.email,
|
||||
disabled = EXCLUDED.disabled,
|
||||
image = EXCLUDED.image
|
||||
"#,
|
||||
user.name,
|
||||
user.display_name,
|
||||
user.password,
|
||||
user.email,
|
||||
user.disabled,
|
||||
user.image
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_by_name(
|
||||
pool: &PgPool,
|
||||
name: &str,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
query!(
|
||||
r#"
|
||||
DELETE FROM users
|
||||
WHERE name = $1
|
||||
"#,
|
||||
name
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn all_exist_by_names(
|
||||
pool: &PgPool,
|
||||
names: &[String],
|
||||
) -> Result<bool, Box<dyn Error + Send + Sync>> {
|
||||
let row = query!(
|
||||
r#"
|
||||
SELECT COUNT(*) AS "count!"
|
||||
FROM users
|
||||
WHERE name = ANY($1)
|
||||
"#,
|
||||
names
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.count == i64::try_from(names.len()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Users {
|
||||
pub users: HashMap<NonEmptyString, User>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: Option<HashMap<NonEmptyString, Value>>,
|
||||
pub struct UserWithGroups {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub password: String,
|
||||
pub email: String,
|
||||
#[serde(default)]
|
||||
pub disabled: bool,
|
||||
#[serde(default)]
|
||||
pub image: Option<String>,
|
||||
pub groups: Vec<String>,
|
||||
}
|
||||
|
||||
impl Deref for Users {
|
||||
type Target = HashMap<NonEmptyString, User>;
|
||||
impl UserWithGroups {
|
||||
pub async fn select(pool: &PgPool) -> Result<Vec<Self>, Box<dyn Error + Send + Sync>> {
|
||||
let users = query_as!(
|
||||
UserWithGroups,
|
||||
r#"
|
||||
SELECT
|
||||
u.name,
|
||||
u.display_name,
|
||||
u.password,
|
||||
u.email,
|
||||
u.disabled,
|
||||
u.image,
|
||||
COALESCE(array_agg(ug.group_name ORDER BY ug.group_name), ARRAY[]::TEXT[]) AS "groups!"
|
||||
FROM users u
|
||||
LEFT JOIN users_groups ug ON u.name = ug.user_name
|
||||
GROUP BY u.name, u.email, u.disabled, u.image
|
||||
"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.users
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Users {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.users
|
||||
}
|
||||
}
|
||||
|
||||
impl From<super::authelia::UserFile> for User {
|
||||
fn from(user_file: super::authelia::UserFile) -> Self {
|
||||
Self {
|
||||
displayname: user_file.displayname,
|
||||
email: user_file.email,
|
||||
password: user_file.password,
|
||||
disabled: user_file.disabled.unwrap_or(false),
|
||||
groups: user_file.groups.unwrap_or_default(),
|
||||
extra: user_file.extra,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<super::authelia::UsersFile> for Users {
|
||||
fn from(users_file: super::authelia::UsersFile) -> Self {
|
||||
Self {
|
||||
users: users_file
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|(key, user)| (key, User::from(user)))
|
||||
.collect(),
|
||||
extra: users_file.extra,
|
||||
}
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn select_by_name(
|
||||
pool: &PgPool,
|
||||
name: &str,
|
||||
) -> Result<Option<Self>, Box<dyn Error + Send + Sync>> {
|
||||
let user = query_as!(
|
||||
UserWithGroups,
|
||||
r#"
|
||||
SELECT
|
||||
u.name,
|
||||
u.display_name,
|
||||
u.password,
|
||||
u.email,
|
||||
u.disabled,
|
||||
u.image,
|
||||
COALESCE(array_agg(ug.group_name ORDER BY ug.group_name), ARRAY[]::TEXT[]) AS "groups!"
|
||||
FROM users u
|
||||
LEFT JOIN users_groups ug ON u.name = ug.user_name
|
||||
WHERE u.name = $1
|
||||
GROUP BY u.name, u.email, u.disabled, u.image
|
||||
"#,
|
||||
name
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
pool: &PgPool,
|
||||
user_with_groups: &Self,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
query!(
|
||||
r#"INSERT INTO users (name, display_name, password, email, disabled, image)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
"#,
|
||||
user_with_groups.name,
|
||||
user_with_groups.display_name,
|
||||
user_with_groups.password,
|
||||
user_with_groups.email,
|
||||
user_with_groups.disabled,
|
||||
user_with_groups.image
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
query!(
|
||||
r#"
|
||||
INSERT INTO users_groups (user_name, group_name)
|
||||
SELECT * FROM UNNEST($1::text[], $2::text[])
|
||||
"#,
|
||||
&user_with_groups.groups,
|
||||
&vec![user_with_groups.name.clone(); user_with_groups.groups.len()]
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
30
src/state.rs
30
src/state.rs
@@ -11,6 +11,7 @@ use openidconnect::{
|
||||
reqwest,
|
||||
};
|
||||
use redis::{self, AsyncCommands};
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
use tokio::spawn;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -47,6 +48,7 @@ pub struct State {
|
||||
pub config: Config,
|
||||
pub oauth_http_client: reqwest::Client,
|
||||
pub oauth_client: OAuthClient,
|
||||
pub pg_pool: PgPool,
|
||||
pub redis_client: redis::aio::MultiplexedConnection,
|
||||
pub session_store: RedisSessionStore,
|
||||
}
|
||||
@@ -54,6 +56,7 @@ pub struct State {
|
||||
impl State {
|
||||
pub async fn from_config(config: Config) -> Self {
|
||||
let (oauth_http_client, oauth_client) = oauth_client(&config).await;
|
||||
let pg_pool = pg_pool(&config).await;
|
||||
let redis_client = redis_client(&config).await;
|
||||
let session_store = session_store(&config);
|
||||
|
||||
@@ -61,6 +64,7 @@ impl State {
|
||||
config,
|
||||
oauth_http_client,
|
||||
oauth_client,
|
||||
pg_pool,
|
||||
redis_client,
|
||||
session_store,
|
||||
}
|
||||
@@ -85,6 +89,12 @@ impl FromRef<State> for OAuthClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<State> for PgPool {
|
||||
fn from_ref(state: &State) -> Self {
|
||||
state.pg_pool.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<State> for redis::aio::MultiplexedConnection {
|
||||
fn from_ref(state: &State) -> Self {
|
||||
state.redis_client.clone()
|
||||
@@ -127,6 +137,21 @@ async fn oauth_client(config: &Config) -> (reqwest::Client, OAuthClient) {
|
||||
(oauth_http_client, oauth_client)
|
||||
}
|
||||
|
||||
async fn pg_pool(config: &Config) -> PgPool {
|
||||
PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
config.postgresql.user,
|
||||
config.postgresql.password,
|
||||
config.postgresql.host,
|
||||
config.postgresql.port,
|
||||
config.postgresql.database
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn redis_client(config: &Config) -> redis::aio::MultiplexedConnection {
|
||||
let url = format!(
|
||||
"redis://{}:{}/{}",
|
||||
@@ -153,7 +178,7 @@ async fn redis_client(config: &Config) -> redis::aio::MultiplexedConnection {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channel = format!("__keyevent@{}__:expired", database);
|
||||
let channel = format!("__keyevent@{database}__:expired");
|
||||
connection.subscribe(&[channel]).await.unwrap();
|
||||
|
||||
while let Some(msg) = rx.recv().await {
|
||||
@@ -178,7 +203,6 @@ fn session_store(config: &Config) -> RedisSessionStore {
|
||||
"redis://{}:{}/{}",
|
||||
config.redis.host, config.redis.port, config.redis.database
|
||||
);
|
||||
let session_store = RedisSessionStore::new(url).unwrap().with_prefix("session:");
|
||||
|
||||
session_store
|
||||
RedisSessionStore::new(url).unwrap().with_prefix("session:")
|
||||
}
|
||||
|
@@ -1,39 +0,0 @@
|
||||
use std::error::Error;
|
||||
|
||||
use crate::{models, state::State};
|
||||
|
||||
impl State {
|
||||
pub fn load_users(&self) -> Result<models::users::Users, Box<dyn Error + Send + Sync>> {
|
||||
let file_contents = std::fs::read_to_string(&self.config.authelia.user_database)?;
|
||||
let users_file: models::authelia::UsersFile = serde_yaml::from_str(&file_contents)?;
|
||||
let users = models::users::Users::from(users_file);
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub fn load_groups(&self) -> Result<models::groups::Groups, Box<dyn Error + Send + Sync>> {
|
||||
let file_contents = std::fs::read_to_string(&self.config.authelia.user_database)?;
|
||||
let users_file = serde_yaml::from_str::<models::authelia::UsersFile>(&file_contents)?;
|
||||
let groups = models::groups::Groups::from(users_file);
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
pub fn load_users_and_groups(
|
||||
&self,
|
||||
) -> Result<(models::users::Users, models::groups::Groups), Box<dyn Error + Send + Sync>> {
|
||||
let file_contents = std::fs::read_to_string(&self.config.authelia.user_database)?;
|
||||
let users_file = serde_yaml::from_str::<models::authelia::UsersFile>(&file_contents)?;
|
||||
let users = models::users::Users::from(users_file.clone());
|
||||
let groups = models::groups::Groups::from(users_file);
|
||||
Ok((users, groups))
|
||||
}
|
||||
|
||||
pub fn save_users(
|
||||
&self,
|
||||
users: models::users::Users,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let users_file = models::authelia::UsersFile::from(users);
|
||||
let file_contents = serde_yaml::to_string(&users_file)?;
|
||||
std::fs::write(&self.config.authelia.user_database, file_contents)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,2 +1 @@
|
||||
pub mod authelia;
|
||||
pub mod crypto;
|
||||
|
Reference in New Issue
Block a user