change handle endpoint & why was changepassword a GET?

This commit is contained in:
2026-03-01 14:53:54 +01:00
parent b2a80ffa58
commit e60172527c
5 changed files with 48 additions and 5 deletions

View File

@@ -23,7 +23,8 @@ pub fn api_router() -> Router {
.route("/api/users/me", get(users::get_me)) .route("/api/users/me", get(users::get_me))
.route("/api/users/{id}", get(users::get_by_id)) .route("/api/users/{id}", get(users::get_by_id))
.route("/api/users/@{handle}", get(users::get_by_handle)) .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}", get(sessions::get_by_id))
.route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id)) .route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id))
.route("/api/tags/{id}", get(tags::get_by_id)) .route("/api/tags/{id}", get(tags::get_by_id))

View File

@@ -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 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."; const PASSW_CHANGED_SUCCESS: &str = "Password changed successfully.";
pub async fn get_me(headers: HeaderMap) -> Result<Response, CompositeError> { pub async fn get_me(headers: HeaderMap) -> Result<Response, CompositeError> {
@@ -40,6 +42,28 @@ pub async fn get_by_handle(
Ok(Json(User::get_by_handle(handle)?).into_response()) Ok(Json(User::get_by_handle(handle)?).into_response())
} }
#[derive(Deserialize)]
pub struct ChangeHandleForm {
handle: UserHandle,
}
pub async fn change_handle(
Path(id): Path<Uuid>,
headers: HeaderMap,
Json(form): Json<ChangeHandleForm>,
) -> Result<Response, CompositeError> {
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)] #[derive(Deserialize)]
pub struct ChangePasswordForm { pub struct ChangePasswordForm {
password: String, password: String,

View File

@@ -2,7 +2,7 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use rusqlite::OptionalExtension; use rusqlite::{ErrorCode, OptionalExtension};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@@ -35,6 +35,8 @@ pub enum UserError {
NoUserWithId(Uuid), NoUserWithId(Uuid),
#[error("No user found with handle {0}")] #[error("No user found with handle {0}")]
NoUserWithHandle(UserHandle), NoUserWithHandle(UserHandle),
#[error("A user with handle {0} already exists")]
HandleAlreadyExists(UserHandle),
#[error("Database error: {0}")] #[error("Database error: {0}")]
DatabaseError(String), DatabaseError(String),
#[error("Argon2 passhash error: {0}")] #[error("Argon2 passhash error: {0}")]
@@ -72,6 +74,21 @@ impl User {
None => Err(UserError::NoUserWithHandle(handle)), 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 // DANGEROUS: AUTH
@@ -123,7 +140,6 @@ impl User {
/// to do everything and probably should not be used as a regular account /// 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, /// due to the ramifications of compromise. But it could be used for that,
/// and have its name changed. /// and have its name changed.
#[allow(unused)]
pub fn is_infradmin(&self) -> bool { pub fn is_infradmin(&self) -> bool {
self.id == Uuid::max() self.id == Uuid::max()
} }
@@ -169,7 +185,6 @@ impl User {
/// for actions performed by Mnemosyne internally. /// for actions performed by Mnemosyne internally.
/// It shall not be available for log-in. /// It shall not be available for log-in.
/// It should not have its name changed, and should be protected from that. /// It should not have its name changed, and should be protected from that.
#[allow(unused)]
pub fn is_systemuser(&self) -> bool { pub fn is_systemuser(&self) -> bool {
self.id == Uuid::nil() self.id == Uuid::nil()
} }
@@ -199,6 +214,7 @@ impl IntoResponse for UserError {
Self::UserHandleError(_) => (StatusCode::BAD_REQUEST, self.to_string()), Self::UserHandleError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
Self::NoUserWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()), Self::NoUserWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()),
Self::NoUserWithHandle(_) => (StatusCode::BAD_REQUEST, self.to_string()), Self::NoUserWithHandle(_) => (StatusCode::BAD_REQUEST, self.to_string()),
Self::HandleAlreadyExists(_) => (StatusCode::CONFLICT, self.to_string()),
} }
.into_response() .into_response()
} }

View File

@@ -8,10 +8,13 @@ pub enum Permission {
RevokeOthersSessions, RevokeOthersSessions,
// All Users have the right to change their own password // All Users have the right to change their own password
ChangeOthersPasswords, ChangeOthersPasswords,
// All Users have the right to change their own handle
ChangeOthersHandles,
} }
impl User { impl User {
pub fn has_permission(&self, permission: Permission) -> Result<bool, DatabaseError> { pub fn has_permission(&self, permission: Permission) -> Result<bool, DatabaseError> {
// Infradmin and systemuser have all permissions
if self.is_infradmin() || self.is_systemuser() { if self.is_infradmin() || self.is_systemuser() {
return Ok(true); return Ok(true);
} }

View File

@@ -139,7 +139,6 @@ impl Session {
Ok(()) Ok(())
} }
#[allow(unused)]
pub fn revoke(&mut self, actor: Option<&User>) -> Result<(), SessionError> { pub fn revoke(&mut self, actor: Option<&User>) -> Result<(), SessionError> {
let now = Utc::now(); let now = Utc::now();
let id = actor.map(|u| u.id).unwrap_or(Uuid::nil()); let id = actor.map(|u| u.id).unwrap_or(Uuid::nil());