diff options
Diffstat (limited to 'src/api/users.rs')
| -rw-r--r-- | src/api/users.rs | 544 |
1 files changed, 272 insertions, 272 deletions
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<str>, -} - -impl From<User> 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<Box<str>>, - limit: Option<u32>, - offset: Option<u32>, -} - -#[get("")] -async fn search_users(params: web::Query<SearchUsers>, conn: web::Data<MySqlPool>) -> 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<Uuid>, - conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UserNotFoundError> { - 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<Uuid>, - conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UserNotFoundError> { - 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<str>, - password: Box<str>, -} - -#[derive(Debug, Clone, Error)] -#[error("An account with the given username already exists.")] -struct UsernameTakenError { - username: Box<str>, -} - -impl ResponseError for UsernameTakenError { - fn status_code(&self) -> StatusCode { - StatusCode::CONFLICT - } -} - -#[post("")] -async fn create_user( - body: web::Json<UserRequest>, - conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UsernameTakenError> { - 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<Uuid>, - body: web::Json<UserRequest>, - conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UpdateUserError> { - 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<Uuid>, - body: web::Json<Box<str>>, - conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UpdateUserError> { - 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<Uuid>, - body: web::Json<Box<str>>, - conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UserNotFoundError> { - 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<str>,
+}
+
+impl From<User> 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<Box<str>>,
+ limit: Option<u32>,
+ offset: Option<u32>,
+}
+
+#[get("")]
+async fn search_users(params: web::Query<SearchUsers>, conn: web::Data<MySqlPool>) -> 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<Uuid>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UserNotFoundError> {
+ 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<Uuid>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UserNotFoundError> {
+ 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<str>,
+ password: Box<str>,
+}
+
+#[derive(Debug, Clone, Error)]
+#[error("An account with the given username already exists.")]
+struct UsernameTakenError {
+ username: Box<str>,
+}
+
+impl ResponseError for UsernameTakenError {
+ fn status_code(&self) -> StatusCode {
+ StatusCode::CONFLICT
+ }
+}
+
+#[post("")]
+async fn create_user(
+ body: web::Json<UserRequest>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UsernameTakenError> {
+ 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<Uuid>,
+ body: web::Json<UserRequest>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateUserError> {
+ 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<Uuid>,
+ body: web::Json<Box<str>>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateUserError> {
+ 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<Uuid>,
+ body: web::Json<Box<str>>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UserNotFoundError> {
+ 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)
+}
|
