291 lines
9.2 KiB
Rust
291 lines
9.2 KiB
Rust
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use chrono::{DateTime, NaiveDate};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{PgConnection, Row};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{
|
|
ISE_MSG,
|
|
database::DatabaseError,
|
|
users::{
|
|
auth::UserPasswordHashing,
|
|
handle::{UserHandle, UserHandleError},
|
|
},
|
|
};
|
|
|
|
pub mod auth;
|
|
pub mod handle;
|
|
pub mod permissions;
|
|
pub mod sessions;
|
|
pub mod setup;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct User {
|
|
pub id: Uuid,
|
|
pub handle: UserHandle,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum UserError {
|
|
#[error("UserHandleError: {0}")]
|
|
UserHandleError(#[from] UserHandleError),
|
|
#[error("No user found with ID {0}")]
|
|
NoUserWithId(Uuid),
|
|
#[error("No user found with handle {0}")]
|
|
NoUserWithHandle(UserHandle),
|
|
#[error("A user with this handle already exists")]
|
|
HandleAlreadyExists,
|
|
#[error("{0}")]
|
|
DatabaseError(#[from] DatabaseError),
|
|
#[error("Argon2 passhash error: {0}")]
|
|
PassHashError(argon2::password_hash::Error),
|
|
}
|
|
|
|
impl User {
|
|
pub async fn total_count(conn: &mut PgConnection) -> Result<i64, UserError> {
|
|
Ok(sqlx::query_scalar("SELECT COUNT(*) FROM users")
|
|
.fetch_one(conn)
|
|
.await?)
|
|
}
|
|
|
|
pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> Result<User, UserError> {
|
|
let res = sqlx::query("SELECT handle FROM users WHERE id = $1")
|
|
.bind(id)
|
|
.fetch_optional(conn)
|
|
.await?;
|
|
|
|
match res {
|
|
Some(r) => {
|
|
let handle_str: String = r.try_get("handle")?;
|
|
Ok(User {
|
|
id,
|
|
handle: UserHandle::new(&handle_str)?,
|
|
})
|
|
}
|
|
None => Err(UserError::NoUserWithId(id)),
|
|
}
|
|
}
|
|
|
|
pub async fn get_by_handle(
|
|
conn: &mut PgConnection,
|
|
handle: UserHandle,
|
|
) -> Result<User, UserError> {
|
|
let res = sqlx::query("SELECT id FROM users WHERE handle = $1")
|
|
.bind(handle.as_str())
|
|
.fetch_optional(conn)
|
|
.await?;
|
|
|
|
match res {
|
|
Some(r) => Ok(User {
|
|
id: r.try_get("id")?,
|
|
handle,
|
|
}),
|
|
None => Err(UserError::NoUserWithHandle(handle)),
|
|
}
|
|
}
|
|
|
|
pub async fn get_all(conn: &mut PgConnection) -> Result<Vec<User>, UserError> {
|
|
let rows = sqlx::query("SELECT id, handle FROM users ORDER BY id")
|
|
.fetch_all(conn)
|
|
.await?;
|
|
|
|
let mut users = Vec::with_capacity(rows.len());
|
|
for r in rows {
|
|
let handle_str: String = r.try_get("handle")?;
|
|
users.push(User {
|
|
id: r.try_get("id")?,
|
|
handle: UserHandle::new(&handle_str)?,
|
|
});
|
|
}
|
|
|
|
Ok(users)
|
|
}
|
|
|
|
pub async fn create(conn: &mut PgConnection, handle: UserHandle) -> Result<User, UserError> {
|
|
let id = Uuid::now_v7();
|
|
sqlx::query("INSERT INTO users(id, handle) VALUES ($1, $2)")
|
|
.bind(id)
|
|
.bind(handle.as_str())
|
|
.execute(conn)
|
|
.await?;
|
|
|
|
Ok(User { id, handle })
|
|
}
|
|
|
|
pub async fn set_handle(
|
|
&mut self,
|
|
conn: &mut PgConnection,
|
|
new_handle: UserHandle,
|
|
) -> Result<(), UserError> {
|
|
sqlx::query("UPDATE users SET handle = $1 WHERE id = $2")
|
|
.bind(new_handle.as_str())
|
|
.bind(self.id)
|
|
.execute(conn)
|
|
.await?;
|
|
|
|
self.handle = new_handle;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn created_at(&self) -> Option<NaiveDate> {
|
|
self.id
|
|
.get_timestamp()
|
|
.and_then(|ts| DateTime::from_timestamp(ts.to_unix().0 as i64, 0))
|
|
.map(|dt| dt.date_naive())
|
|
}
|
|
}
|
|
|
|
// DANGEROUS: AUTH
|
|
impl User {
|
|
pub async fn set_password(
|
|
&mut self,
|
|
conn: &mut PgConnection,
|
|
passw: Option<&str>,
|
|
) -> Result<(), UserError> {
|
|
match passw {
|
|
None => {
|
|
sqlx::query("UPDATE users SET password = NULL WHERE id = $1")
|
|
.bind(self.id)
|
|
.execute(conn)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
Some(passw) => {
|
|
let hashed = User::hash_password(passw)?;
|
|
sqlx::query("UPDATE users SET password = $1 WHERE id = $2")
|
|
.bind(hashed)
|
|
.bind(self.id)
|
|
.execute(conn)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// RESERVED USERS IMPL
|
|
impl User {
|
|
/// Constructs and pushes an infradmin to database
|
|
///
|
|
/// An infradmin is the user account made for controlling
|
|
/// Mnemosyne top-down. The infrastructure admin has permission
|
|
/// to do everything and probably should not be used as a regular account
|
|
/// due to the ramifications of compromise. But it could be used for that,
|
|
/// and have its name changed.
|
|
pub async fn create_infradmin(conn: &mut PgConnection) -> Result<User, UserError> {
|
|
let mut u = User {
|
|
id: Uuid::max(),
|
|
handle: UserHandle::new("Infradmin")?,
|
|
};
|
|
sqlx::query("INSERT INTO users(id, handle) VALUES ($1, $2)")
|
|
.bind(u.id)
|
|
.bind(u.handle.as_str())
|
|
.execute(&mut *conn)
|
|
.await?;
|
|
|
|
u.regenerate_infradmin_password(conn).await?;
|
|
|
|
Ok(u)
|
|
}
|
|
|
|
/// Checks if the User is an infradmin
|
|
///
|
|
/// An infradmin is the user account made for controlling
|
|
/// Mnemosyne top-down. The infrastructure admin has permission
|
|
/// to do everything and probably should not be used as a regular account
|
|
/// due to the ramifications of compromise. But it could be used for that,
|
|
/// and have its name changed.
|
|
pub fn is_infradmin(&self) -> bool {
|
|
self.id == Uuid::max()
|
|
}
|
|
|
|
/// Regenerates the infradmin password
|
|
///
|
|
/// An infradmin is the user account made for controlling
|
|
/// Mnemosyne top-down. The infrastructure admin has permission
|
|
/// to do everything and probably should not be used as a regular account
|
|
/// due to the ramifications of compromise. But it could be used for that,
|
|
/// and have its name changed.
|
|
pub async fn regenerate_infradmin_password(
|
|
&mut self,
|
|
conn: &mut PgConnection,
|
|
) -> Result<(), UserError> {
|
|
let passw = auth::generate_token(auth::TokenSize::Char16);
|
|
self.set_password(conn, Some(&passw)).await?;
|
|
log::info!("[USERS] The infradmin account password has been (re)generated.");
|
|
log::info!("[USERS] Handle: {}", self.handle.as_str());
|
|
log::info!("[USERS] Password: {}", passw);
|
|
log::info!("[USERS] The infradmin is urged to change this password to a secure one.");
|
|
Ok(())
|
|
}
|
|
|
|
/// Constructs and pushes a systemuser to database
|
|
///
|
|
/// A systemuser is used for internal blame representation
|
|
/// for actions performed by Mnemosyne internally.
|
|
/// It shall not be available for log-in.
|
|
/// It should not have its name changed, and should be protected from that.
|
|
pub async fn create_systemuser(conn: &mut PgConnection) -> Result<User, UserError> {
|
|
let u = User {
|
|
id: Uuid::nil(),
|
|
handle: UserHandle::new("Mnemosyne")?,
|
|
};
|
|
sqlx::query("INSERT INTO users(id, handle) VALUES ($1, $2)")
|
|
.bind(u.id)
|
|
.bind(u.handle.as_str())
|
|
.execute(conn)
|
|
.await?;
|
|
|
|
Ok(u)
|
|
}
|
|
|
|
/// Checks if the User is a systemuser
|
|
///
|
|
/// A systemuser is used for internal blame representation
|
|
/// for actions performed by Mnemosyne internally.
|
|
/// It shall not be available for log-in.
|
|
/// It should not have its name changed, and should be protected from that.
|
|
pub fn is_systemuser(&self) -> bool {
|
|
self.id == Uuid::nil()
|
|
}
|
|
}
|
|
|
|
impl From<sqlx::Error> for UserError {
|
|
fn from(error: sqlx::Error) -> Self {
|
|
if let sqlx::Error::Database(err) = &error {
|
|
// Check for Postgres unique constraint violation (code 23505)
|
|
if err.is_unique_violation() && err.message().contains("handle") {
|
|
return UserError::HandleAlreadyExists;
|
|
}
|
|
}
|
|
UserError::DatabaseError(DatabaseError::from(error))
|
|
}
|
|
}
|
|
|
|
impl From<argon2::password_hash::Error> for UserError {
|
|
fn from(err: argon2::password_hash::Error) -> Self {
|
|
UserError::PassHashError(err)
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for UserError {
|
|
fn into_response(self) -> Response {
|
|
match self {
|
|
Self::DatabaseError(e) => e.into_response(),
|
|
Self::PassHashError(e) => {
|
|
log::error!("[PASSHASH] A passwordhash error occured: {e}");
|
|
(StatusCode::INTERNAL_SERVER_ERROR, ISE_MSG.to_string()).into_response()
|
|
}
|
|
Self::UserHandleError(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
|
Self::NoUserWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
|
Self::NoUserWithHandle(_) => {
|
|
(StatusCode::BAD_REQUEST, self.to_string()).into_response()
|
|
}
|
|
Self::HandleAlreadyExists => (StatusCode::CONFLICT, self.to_string()).into_response(),
|
|
}
|
|
}
|
|
}
|