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 { Ok(sqlx::query_scalar("SELECT COUNT(*) FROM users") .fetch_one(conn) .await?) } pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> Result { 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 { 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, 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 { 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 { 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 { 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 { 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 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 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(), } } }