From 608ce1d9910cd68ce825838ea313e02c598f908e Mon Sep 17 00:00:00 2001 From: Mica White Date: Mon, 8 Dec 2025 20:08:21 -0500 Subject: Stuff --- src/api/users.rs | 544 +++++++++++++++++++++++++++---------------------------- 1 file changed, 272 insertions(+), 272 deletions(-) (limited to 'src/api/users.rs') diff --git a/src/api/users.rs b/src/api/users.rs index 391a059..da2a0d0 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -1,272 +1,272 @@ -use actix_web::http::{header, StatusCode}; -use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; -use raise::yeet; -use serde::{Deserialize, Serialize}; -use sqlx::MySqlPool; -use thiserror::Error; -use uuid::Uuid; - -use crate::models::user::User; -use crate::services::crypto::PasswordHash; -use crate::services::{db, id}; - -/// Just a username. No password hash, because that'd be tempting fate. -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct UserResponse { - id: Uuid, - username: Box, -} - -impl From for UserResponse { - fn from(user: User) -> Self { - Self { - id: user.id, - username: user.username, - } - } -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SearchUsers { - username: Option>, - limit: Option, - offset: Option, -} - -#[get("")] -async fn search_users(params: web::Query, conn: web::Data) -> HttpResponse { - let conn = conn.get_ref(); - - let username = params.username.clone().unwrap_or_default(); - let offset = params.offset.unwrap_or_default(); - - let results: Box<[UserResponse]> = if let Some(limit) = params.limit { - db::search_users_limit(conn, &username, offset, limit) - .await - .unwrap() - .iter() - .cloned() - .map(|u| u.into()) - .collect() - } else { - db::search_users(conn, &username) - .await - .unwrap() - .into_iter() - .skip(offset as usize) - .cloned() - .map(|u| u.into()) - .collect() - }; - - let response = HttpResponse::Ok().json(results); - response -} - -#[derive(Debug, Clone, Error)] -#[error("No user with the given ID exists")] -struct UserNotFoundError { - user_id: Uuid, -} - -impl ResponseError for UserNotFoundError { - fn status_code(&self) -> StatusCode { - StatusCode::NOT_FOUND - } -} - -#[get("/{user_id}")] -async fn get_user( - user_id: web::Path, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let id = user_id.to_owned(); - let username = db::get_username(conn, id).await.unwrap(); - - let Some(username) = username else { - yeet!(UserNotFoundError { user_id: id }); - }; - - let response = UserResponse { id, username }; - let response = HttpResponse::Ok().json(response); - Ok(response) -} - -#[get("/{user_id}/username")] -async fn get_username( - user_id: web::Path, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let username = db::get_username(conn, user_id).await.unwrap(); - - let Some(username) = username else { - yeet!(UserNotFoundError { user_id }); - }; - - let response = HttpResponse::Ok().json(username); - Ok(response) -} - -/// A request to create or update user information -#[derive(Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct UserRequest { - username: Box, - password: Box, -} - -#[derive(Debug, Clone, Error)] -#[error("An account with the given username already exists.")] -struct UsernameTakenError { - username: Box, -} - -impl ResponseError for UsernameTakenError { - fn status_code(&self) -> StatusCode { - StatusCode::CONFLICT - } -} - -#[post("")] -async fn create_user( - body: web::Json, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = id::new_id(conn, db::user_id_exists).await.unwrap(); - let username = body.username.clone(); - let password = PasswordHash::new(&body.password).unwrap(); - - if db::username_is_used(conn, &body.username).await.unwrap() { - yeet!(UsernameTakenError { username }); - } - - let user = User { - id: user_id, - username, - password, - }; - - db::create_user(conn, &user).await.unwrap(); - - let response = HttpResponse::Created() - .insert_header((header::LOCATION, format!("users/{user_id}"))) - .finish(); - Ok(response) -} - -#[derive(Debug, Clone, Error)] -enum UpdateUserError { - #[error(transparent)] - UsernameTaken(#[from] UsernameTakenError), - #[error(transparent)] - NotFound(#[from] UserNotFoundError), -} - -impl ResponseError for UpdateUserError { - fn status_code(&self) -> StatusCode { - match self { - Self::UsernameTaken(e) => e.status_code(), - Self::NotFound(e) => e.status_code(), - } - } -} - -#[put("/{user_id}")] -async fn update_user( - user_id: web::Path, - body: web::Json, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let username = body.username.clone(); - let password = PasswordHash::new(&body.password).unwrap(); - - let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); - if username != old_username && db::username_is_used(conn, &body.username).await.unwrap() { - yeet!(UsernameTakenError { username }.into()) - } - - if !db::user_id_exists(conn, user_id).await.unwrap() { - yeet!(UserNotFoundError { user_id }.into()) - } - - let user = User { - id: user_id, - username, - password, - }; - - db::update_user(conn, &user).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -#[put("/{user_id}/username")] -async fn update_username( - user_id: web::Path, - body: web::Json>, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let username = body.clone(); - - let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); - if username != old_username && db::username_is_used(conn, &body).await.unwrap() { - yeet!(UsernameTakenError { username }.into()) - } - - if !db::user_id_exists(conn, user_id).await.unwrap() { - yeet!(UserNotFoundError { user_id }.into()) - } - - db::update_username(conn, user_id, &body).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -#[put("/{user_id}/password")] -async fn update_password( - user_id: web::Path, - body: web::Json>, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let password = PasswordHash::new(&body).unwrap(); - - if !db::user_id_exists(conn, user_id).await.unwrap() { - yeet!(UserNotFoundError { user_id }) - } - - db::update_password(conn, user_id, &password).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -pub fn service() -> Scope { - web::scope("/users") - .service(search_users) - .service(get_user) - .service(get_username) - .service(create_user) - .service(update_user) - .service(update_username) - .service(update_password) -} +use actix_web::http::{header, StatusCode}; +use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; +use raise::yeet; +use serde::{Deserialize, Serialize}; +use sqlx::MySqlPool; +use thiserror::Error; +use uuid::Uuid; + +use crate::models::user::User; +use crate::services::crypto::PasswordHash; +use crate::services::{db, id}; + +/// Just a username. No password hash, because that'd be tempting fate. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct UserResponse { + id: Uuid, + username: Box, +} + +impl From for UserResponse { + fn from(user: User) -> Self { + Self { + id: user.id, + username: user.username, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SearchUsers { + username: Option>, + limit: Option, + offset: Option, +} + +#[get("")] +async fn search_users(params: web::Query, conn: web::Data) -> HttpResponse { + let conn = conn.get_ref(); + + let username = params.username.clone().unwrap_or_default(); + let offset = params.offset.unwrap_or_default(); + + let results: Box<[UserResponse]> = if let Some(limit) = params.limit { + db::search_users_limit(conn, &username, offset, limit) + .await + .unwrap() + .iter() + .cloned() + .map(|u| u.into()) + .collect() + } else { + db::search_users(conn, &username) + .await + .unwrap() + .into_iter() + .skip(offset as usize) + .cloned() + .map(|u| u.into()) + .collect() + }; + + let response = HttpResponse::Ok().json(results); + response +} + +#[derive(Debug, Clone, Error)] +#[error("No user with the given ID exists")] +struct UserNotFoundError { + user_id: Uuid, +} + +impl ResponseError for UserNotFoundError { + fn status_code(&self) -> StatusCode { + StatusCode::NOT_FOUND + } +} + +#[get("/{user_id}")] +async fn get_user( + user_id: web::Path, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let id = user_id.to_owned(); + let username = db::get_username(conn, id).await.unwrap(); + + let Some(username) = username else { + yeet!(UserNotFoundError { user_id: id }); + }; + + let response = UserResponse { id, username }; + let response = HttpResponse::Ok().json(response); + Ok(response) +} + +#[get("/{user_id}/username")] +async fn get_username( + user_id: web::Path, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let username = db::get_username(conn, user_id).await.unwrap(); + + let Some(username) = username else { + yeet!(UserNotFoundError { user_id }); + }; + + let response = HttpResponse::Ok().json(username); + Ok(response) +} + +/// A request to create or update user information +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct UserRequest { + username: Box, + password: Box, +} + +#[derive(Debug, Clone, Error)] +#[error("An account with the given username already exists.")] +struct UsernameTakenError { + username: Box, +} + +impl ResponseError for UsernameTakenError { + fn status_code(&self) -> StatusCode { + StatusCode::CONFLICT + } +} + +#[post("")] +async fn create_user( + body: web::Json, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = id::new_id(conn, db::user_id_exists).await.unwrap(); + let username = body.username.clone(); + let password = PasswordHash::new(&body.password).unwrap(); + + if db::username_is_used(conn, &body.username).await.unwrap() { + yeet!(UsernameTakenError { username }); + } + + let user = User { + id: user_id, + username, + password, + }; + + db::create_user(conn, &user).await.unwrap(); + + let response = HttpResponse::Created() + .insert_header((header::LOCATION, format!("users/{user_id}"))) + .finish(); + Ok(response) +} + +#[derive(Debug, Clone, Error)] +enum UpdateUserError { + #[error(transparent)] + UsernameTaken(#[from] UsernameTakenError), + #[error(transparent)] + NotFound(#[from] UserNotFoundError), +} + +impl ResponseError for UpdateUserError { + fn status_code(&self) -> StatusCode { + match self { + Self::UsernameTaken(e) => e.status_code(), + Self::NotFound(e) => e.status_code(), + } + } +} + +#[put("/{user_id}")] +async fn update_user( + user_id: web::Path, + body: web::Json, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let username = body.username.clone(); + let password = PasswordHash::new(&body.password).unwrap(); + + let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); + if username != old_username && db::username_is_used(conn, &body.username).await.unwrap() { + yeet!(UsernameTakenError { username }.into()) + } + + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }.into()) + } + + let user = User { + id: user_id, + username, + password, + }; + + db::update_user(conn, &user).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{user_id}/username")] +async fn update_username( + user_id: web::Path, + body: web::Json>, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let username = body.clone(); + + let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); + if username != old_username && db::username_is_used(conn, &body).await.unwrap() { + yeet!(UsernameTakenError { username }.into()) + } + + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }.into()) + } + + db::update_username(conn, user_id, &body).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{user_id}/password")] +async fn update_password( + user_id: web::Path, + body: web::Json>, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let password = PasswordHash::new(&body).unwrap(); + + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }) + } + + db::update_password(conn, user_id, &password).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +pub fn service() -> Scope { + web::scope("/users") + .service(search_users) + .service(get_user) + .service(get_username) + .service(create_user) + .service(update_user) + .service(update_username) + .service(update_password) +} -- cgit v1.2.3