diff --git a/src/api/mod.rs b/src/api/mod.rs index dc27492..8b4a76c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -23,7 +23,8 @@ pub fn api_router() -> Router { .route("/api/users/me", get(users::get_me)) .route("/api/users/{id}", get(users::get_by_id)) .route("/api/users/@{handle}", get(users::get_by_handle)) - .route("/api/users/{id}/setpassw", get(users::change_password)) + .route("/api/users/{id}/setpassw", post(users::change_password)) + .route("/api/users/{id}/sethandle", post(users::change_handle)) .route("/api/sessions/{id}", get(sessions::get_by_id)) .route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id)) .route("/api/tags/{id}", get(tags::get_by_id)) diff --git a/src/api/users.rs b/src/api/users.rs index 1ae2ebf..a1b9ef4 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -17,7 +17,9 @@ use crate::{ }, }; +const CANT_CHANGE_OTHERS_HANDLE: &str = "You don't have permission to change this user's handle."; const CANT_CHANGE_OTHERS_PASSW: &str = "You don't have permission to change this user's password."; +const HANDLE_CHANGED_SUCCESS: &str = "Handle changed successfully."; const PASSW_CHANGED_SUCCESS: &str = "Password changed successfully."; pub async fn get_me(headers: HeaderMap) -> Result { @@ -40,6 +42,28 @@ pub async fn get_by_handle( Ok(Json(User::get_by_handle(handle)?).into_response()) } +#[derive(Deserialize)] +pub struct ChangeHandleForm { + handle: UserHandle, +} +pub async fn change_handle( + Path(id): Path, + headers: HeaderMap, + Json(form): Json, +) -> Result { + let u = User::authenticate(&headers)?.required()?; + let mut target = if u.id == id { + u + } else { + if u.has_permission(Permission::ChangeOthersHandles)? == false { + return Ok((StatusCode::FORBIDDEN, CANT_CHANGE_OTHERS_HANDLE).into_response()); + } + User::get_by_id(id)? + }; + target.set_handle(form.handle)?; + Ok(HANDLE_CHANGED_SUCCESS.into_response()) +} + #[derive(Deserialize)] pub struct ChangePasswordForm { password: String, diff --git a/src/users/mod.rs b/src/users/mod.rs index f562bc6..942b122 100644 --- a/src/users/mod.rs +++ b/src/users/mod.rs @@ -2,7 +2,7 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; -use rusqlite::OptionalExtension; +use rusqlite::{ErrorCode, OptionalExtension}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -35,6 +35,8 @@ pub enum UserError { NoUserWithId(Uuid), #[error("No user found with handle {0}")] NoUserWithHandle(UserHandle), + #[error("A user with handle {0} already exists")] + HandleAlreadyExists(UserHandle), #[error("Database error: {0}")] DatabaseError(String), #[error("Argon2 passhash error: {0}")] @@ -72,6 +74,21 @@ impl User { None => Err(UserError::NoUserWithHandle(handle)), } } + pub fn set_handle(&mut self, new_handle: UserHandle) -> Result<(), UserError> { + let conn = database::conn()?; + conn.prepare("UPDATE users SET handle = ?1 WHERE id = ?2")? + .execute((&new_handle, self.id)) + .map_err(|e| { + if let Some(e) = e.sqlite_error() { + if e.code == ErrorCode::ConstraintViolation { + return UserError::HandleAlreadyExists(new_handle.clone()); + } + } + UserError::from(e) + })?; + self.handle = new_handle; + Ok(()) + } } // DANGEROUS: AUTH @@ -123,7 +140,6 @@ impl User { /// 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. - #[allow(unused)] pub fn is_infradmin(&self) -> bool { self.id == Uuid::max() } @@ -169,7 +185,6 @@ impl User { /// 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. - #[allow(unused)] pub fn is_systemuser(&self) -> bool { self.id == Uuid::nil() } @@ -199,6 +214,7 @@ impl IntoResponse for UserError { Self::UserHandleError(_) => (StatusCode::BAD_REQUEST, self.to_string()), Self::NoUserWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()), Self::NoUserWithHandle(_) => (StatusCode::BAD_REQUEST, self.to_string()), + Self::HandleAlreadyExists(_) => (StatusCode::CONFLICT, self.to_string()), } .into_response() } diff --git a/src/users/permissions.rs b/src/users/permissions.rs index 73dc146..d87ae31 100644 --- a/src/users/permissions.rs +++ b/src/users/permissions.rs @@ -8,10 +8,13 @@ pub enum Permission { RevokeOthersSessions, // All Users have the right to change their own password ChangeOthersPasswords, + // All Users have the right to change their own handle + ChangeOthersHandles, } impl User { pub fn has_permission(&self, permission: Permission) -> Result { + // Infradmin and systemuser have all permissions if self.is_infradmin() || self.is_systemuser() { return Ok(true); } diff --git a/src/users/sessions.rs b/src/users/sessions.rs index bf113b1..142ea4c 100644 --- a/src/users/sessions.rs +++ b/src/users/sessions.rs @@ -139,7 +139,6 @@ impl Session { Ok(()) } - #[allow(unused)] pub fn revoke(&mut self, actor: Option<&User>) -> Result<(), SessionError> { let now = Utc::now(); let id = actor.map(|u| u.id).unwrap_or(Uuid::nil());