From 4caa0c8328537f3bec289bef2c028dc0f44b4ac9 Mon Sep 17 00:00:00 2001 From: Alexander Ng Date: Tue, 10 Dec 2024 03:59:34 -0800 Subject: [PATCH] added error handling --- Cargo.lock | 24 ++++++++-- Cargo.toml | 3 +- src/error.rs | 104 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 8 ++++ src/structs/users.rs | 9 ---- src/v1/auth/signup.rs | 32 ++++++++++--- src/v1/mod.rs | 3 -- 7 files changed, 160 insertions(+), 23 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 36e7f3c..429397b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,12 +121,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http", @@ -174,6 +175,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -708,9 +720,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -1554,6 +1566,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", "webpki-roots", ] @@ -1636,6 +1649,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -1675,6 +1689,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -1700,6 +1715,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fe8390b..8075ce9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.7" +axum = { version = "0.7.9", features = ["macros", "json"] } tokio = { version = "1", features = ["full"] } tower = "0.5" utoipa = { version = "5.2.0", features = ["axum_extras"] } @@ -18,6 +18,7 @@ sqlx = { version = "0.8.2", features = [ "chrono", "runtime-tokio-rustls", "macros", + "uuid", ] } uuid = "1.11.0" chrono = "0.4.39" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..6a4bfcb --- /dev/null +++ b/src/error.rs @@ -0,0 +1,104 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use std::str::Utf8Error; + +#[allow(dead_code)] +pub enum Errors { + TooBig(usize), + SqlxError(sqlx::Error), + Ise(anyhow::Error), + Unimplemented, + Unauthorized, +} + +pub enum AppError { + AnyhowError(AnyhowError), + Error(Errors), +} + +impl From for AppError { + fn from(e: anyhow::Error) -> Self { + AppError::AnyhowError(AnyhowError(e)) + } +} + +impl From for AppError { + fn from(e: sqlx::types::uuid::Error) -> Self { + AppError::Error(Errors::SqlxError(sqlx::Error::Decode(e.into()))) + } +} + +impl From for AppError { + fn from(e: Errors) -> Self { + AppError::Error(e) + } +} + +impl From for AppError { + fn from(e: sqlx::Error) -> Self { + AppError::Error(Errors::SqlxError(e)) + } +} + +impl From for AppError { + fn from(e: Utf8Error) -> Self { + AppError::Error(Errors::Ise(anyhow::Error::from(e))) + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + match self { + AppError::AnyhowError(e) => e.into_response(), + + AppError::Error(e) => match e { + Errors::TooBig(size_limit) => ( + StatusCode::BAD_REQUEST, + format!("Value cannot be greater than {} bytes", size_limit), + ) + .into_response(), + + Errors::SqlxError(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response() + } + + Errors::Ise(e) => { + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + } + + Errors::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), + + Errors::Unimplemented => { + (StatusCode::NOT_IMPLEMENTED, "Not implemented").into_response() + } + }, + } + } +} + +// Make our own error that wraps `anyhow::Error`. +pub struct AnyhowError(anyhow::Error); + +// Tell axum how to convert `AppError` into a response. +impl IntoResponse for AnyhowError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into +// `Result<_, AppError>`. That way you don't need to do that manually. +impl From for AnyhowError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} diff --git a/src/main.rs b/src/main.rs index fc1fbec..077f171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,14 @@ mod v1; mod structs; mod state; mod db; +mod error; + +pub(crate) use anyhow::Context; +pub(crate) use axum::extract::{Json, State}; +pub(crate) use error::{AnyhowError, AppError, Errors}; +pub(crate) use serde::{Deserialize, Serialize}; +pub(crate) use state::AppState; +pub(crate) use utoipa::ToSchema; #[derive(OpenApi)] #[openapi( diff --git a/src/structs/users.rs b/src/structs/users.rs index c307e46..e69de29 100644 --- a/src/structs/users.rs +++ b/src/structs/users.rs @@ -1,9 +0,0 @@ -pub struct User { - pub id: uuid::Uuid, - pub real_name: String, - pub username: String, - pub email: String, - pub password_hash: String, - pub is_admin: bool, - pub created_at: chrono::NaiveDateTime, -} diff --git a/src/v1/auth/signup.rs b/src/v1/auth/signup.rs index da64cb8..2519716 100644 --- a/src/v1/auth/signup.rs +++ b/src/v1/auth/signup.rs @@ -1,11 +1,31 @@ -use axum::{extract::State, response::Response}; +use crate::*; +use sqlx::query; -use crate::state::AppState; - -use super::*; +#[derive(Serialize, Deserialize, ToSchema)] +pub struct SignupBody { + real_name: String, + username: String, + email: String, + password: String, +} /// Sign up #[utoipa::path(get, path = "/signup", responses((status = OK, body = String)), tag = super::AUTH_TAG)] -pub async fn signup(State(state): State) -> Response { - Response::new("Hello, World!".to_string()) +pub async fn signup( + State(state): State, + Json(body): Json, +) -> Result { + let user = query!( + "INSERT INTO users (real_name, username, email, password_hash) + VALUES ($1, $2, $3, $4) + RETURNING id, real_name, username, email, password_hash, is_admin, created_at", + body.real_name, + body.username, + body.email, + body.password, + ) + .fetch_one(&*state.db) + .await?; + + Ok(user.id.to_string()) } diff --git a/src/v1/mod.rs b/src/v1/mod.rs index 0ad43a7..9a5715e 100644 --- a/src/v1/mod.rs +++ b/src/v1/mod.rs @@ -1,6 +1,3 @@ -pub(super) use axum::Json; -pub(super) use serde::Serialize; -pub(super) use utoipa::ToSchema; pub(super) use utoipa_axum::router::OpenApiRouter; pub(super) use utoipa_axum::routes;