diff options
| author | mrw1593 <botahamec@outlook.com> | 2023-05-13 11:59:16 -0400 |
|---|---|---|
| committer | mrw1593 <botahamec@outlook.com> | 2023-05-29 10:45:53 -0400 |
| commit | efe24b616f91121512cb6b2c61cd9b850f943e2d (patch) | |
| tree | 1c91e672f7f5797e1936a8eb8b725f1d26858c2b | |
| parent | 0ed1eec3dc607807bcd9aefd678f744313e2b361 (diff) | |
Create get requests for users
| -rw-r--r-- | src/api/users.rs | 118 | ||||
| -rw-r--r-- | src/services/db.rs | 46 |
2 files changed, 149 insertions, 15 deletions
diff --git a/src/api/users.rs b/src/api/users.rs index fcbaf4d..6e00336 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -1,7 +1,7 @@ use actix_web::http::{header, StatusCode}; -use actix_web::{post, put, web, HttpResponse, ResponseError, Scope}; +use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; use raise::yeet; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use sqlx::MySqlPool; use thiserror::Error; use uuid::Uuid; @@ -10,13 +10,75 @@ use crate::models::User; use crate::services::crypto::PasswordHash; use crate::services::{db, id}; -#[derive(Clone, Deserialize)] +#[derive(Debug, Clone, Serialize)] +struct UserResponse { + username: Box<str>, +} + +impl From<User> for UserResponse { + fn from(user: User) -> Self { + Self { + username: user.username, + } + } +} + +#[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 user_id = user_id.to_owned(); + let user = db::get_user(conn, user_id).await.unwrap(); + + let Some(user) = user else { + yeet!(UserNotFoundError {user_id}); + }; + + let response: UserResponse = user.into(); + 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) +} + +#[derive(Debug, Clone, Deserialize)] struct UserRequest { username: Box<str>, password: Box<str>, } -#[derive(Debug, Clone, Hash, Error)] +#[derive(Debug, Clone, Error)] #[error("An account with the given username already exists.")] struct UsernameTakenError { username: Box<str>, @@ -57,12 +119,29 @@ async fn create_user( 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(..) => StatusCode::CONFLICT, + Self::NotFound(..) => StatusCode::NOT_FOUND, + } + } +} + #[put("/{user_id}")] async fn update_user( user_id: web::Path<Uuid>, body: web::Json<UserRequest>, conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UsernameTakenError> { +) -> Result<HttpResponse, UpdateUserError> { let conn = conn.get_ref(); let user_id = user_id.to_owned(); @@ -71,7 +150,11 @@ async fn update_user( 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 }) + yeet!(UsernameTakenError { username }.into()) + } + + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }.into()) } let user = User { @@ -94,7 +177,7 @@ async fn update_username( user_id: web::Path<Uuid>, body: web::Json<Box<str>>, conn: web::Data<MySqlPool>, -) -> Result<HttpResponse, UsernameTakenError> { +) -> Result<HttpResponse, UpdateUserError> { let conn = conn.get_ref(); let user_id = user_id.to_owned(); @@ -102,7 +185,11 @@ async fn update_username( 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 }) + 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(); @@ -119,23 +206,26 @@ async fn update_password( user_id: web::Path<Uuid>, body: web::Json<Box<str>>, conn: web::Data<MySqlPool>, -) -> HttpResponse { +) -> Result<HttpResponse, UserNotFoundError> { let conn = conn.get_ref(); let user_id = user_id.to_owned(); let password = PasswordHash::new(&body).unwrap(); - db::update_password(conn, user_id, &password).await.unwrap(); + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }) + } - let response = HttpResponse::NoContent() - .insert_header((header::LOCATION, format!("users/{user_id}/password"))) - .finish(); + db::update_password(conn, user_id, &password).await.unwrap(); - response + let response = HttpResponse::NoContent().finish(); + Ok(response) } pub fn service() -> Scope { web::scope("/users") + .service(get_user) + .service(get_username) .service(create_user) .service(update_user) .service(update_username) diff --git a/src/services/db.rs b/src/services/db.rs index a6571b5..3eee9ba 100644 --- a/src/services/db.rs +++ b/src/services/db.rs @@ -1,11 +1,37 @@ use exun::*; -use sqlx::{mysql::MySqlQueryResult, query, query_scalar, Executor, MySql, MySqlPool}; +use sqlx::{mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, MySql, MySqlPool}; use uuid::Uuid; use crate::models::User; use super::crypto::PasswordHash; +struct UserRow { + user_id: Vec<u8>, + username: String, + password_hash: Vec<u8>, + password_salt: Vec<u8>, + password_version: u32, +} + +impl TryFrom<UserRow> for User { + type Error = RawUnexpected; + + fn try_from(row: UserRow) -> Result<Self, Self::Error> { + let password = PasswordHash::from_fields( + &row.password_hash, + &row.password_salt, + row.password_version as u8, + ); + let user = User { + user_id: Uuid::from_slice(&row.user_id)?, + username: row.username.into_boxed_str(), + password, + }; + Ok(user) + } +} + /// Intialize the connection pool pub async fn initialize(db: &str, user: &str, password: &str) -> Result<MySqlPool, RawUnexpected> { let url = format!("mysql://{user}:{password}@localhost/{db}"); @@ -40,6 +66,24 @@ pub async fn username_is_used<'c>( Ok(exists) } +pub async fn get_user<'c>( + conn: impl Executor<'c, Database = MySql>, + user_id: Uuid, +) -> Result<Option<User>, RawUnexpected> { + let record = query_as!( + UserRow, + r"SELECT user_id, username, password_hash, password_salt, password_version + FROM users WHERE user_id = ?", + user_id + ) + .fetch_optional(conn) + .await?; + + let Some(record) = record else { return Ok(None) }; + + Ok(Some(record.try_into()?)) +} + pub async fn get_username<'c>( conn: impl Executor<'c, Database = MySql>, user_id: Uuid, |
