Procházet zdrojové kódy

Create route to create a new user

Lee Morgan před 2 měsíci
rodič
revize
de7a0163bd
5 změnil soubory, kde provedl 58 přidání a 29 odebrání
  1. 1 1
      Cargo.toml
  2. 13 4
      src/app_error.rs
  3. 13 9
      src/controllers/user/create.rs
  4. 10 4
      src/main.rs
  5. 21 11
      src/models/user.rs

+ 1 - 1
Cargo.toml

@@ -19,4 +19,4 @@ sqlx = { version = "0.8.6", features = [
     "uuid"
 ]}
 thiserror = "2.0.18"
-uuid = { version="1.23.1", features=["v7"] }
+uuid = { version="1.23.1", features=["v4", "v7"] }

+ 13 - 4
src/app_error.rs

@@ -13,26 +13,35 @@ struct ErrorInfo {
     message: String
 }
 
-#[derive(Error)]
+#[derive(Error, Debug)]
 pub enum AppError {
     #[error("Internal Server Error")]
     InternalError(String),
 
     #[error("{0}")]
     InvalidInput(String),
+
+    #[error("Internal Server Error")]
+    Database(#[from] sqlx::Error),
+
+    #[error("{0}")]
+    JsonDeserializationError(String)
 }
 
 impl ResponseError for AppError {
     fn status_code(&self) -> StatusCode {
         match self {
-            AppError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
-            AppError::InvalidInput(_) => StatusCode::BAD_REQUEST
+            AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
+            AppError::InvalidInput(_) => StatusCode::BAD_REQUEST,
+            AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
+            AppError::JsonDeserializationError(_) => StatusCode::BAD_REQUEST
         }
     }
 
     fn error_response(&self) -> HttpResponse {
         match self {
             AppError::InternalError(s) => {eprintln!("Internal Error: {}", s)},
+            AppError::Database(s) => {eprintln!("Database Error: {}", s)},
             _ => ()
         };
 
@@ -49,6 +58,6 @@ impl ResponseError for AppError {
 
 impl AppError {
     pub fn invalid_input(msg: &str) -> Self {
-        AppError::InvalidInput(msg.to_owned())
+        AppError::InvalidInput(msg.into())
     }
 }

+ 13 - 9
src/controllers/user/create.rs

@@ -1,9 +1,10 @@
 use actix_web::{HttpResponse, web, post};
-use serde::Serialize;
+use serde::Deserialize;
 use serde_json::json;
 use sqlx::PgPool;
 use email_address::EmailAddress;
 use uuid::Uuid;
+use chrono::{DateTime, Utc, Duration};
 use argon2::{
     Argon2,
     password_hash::{
@@ -17,7 +18,7 @@ use crate::{
     models::user::User
 };
 
-#[derive(Serialize)]
+#[derive(Deserialize)]
 struct Body {
     name: String,
     email: String,
@@ -27,6 +28,7 @@ struct Body {
     time: Option<String>,
 }
 
+#[post("/user")]
 pub async fn route(
     db: web::Data<PgPool>,
     body: web::Json<Body>
@@ -35,17 +37,17 @@ pub async fn route(
         return Ok(HttpResponse::Ok().json(json!({"success": true})));
     }
     valid_password(&body.password, &body.confirm_password)?;
-    let pass_hash = hash_password(body.password)?;
-    let email = validate_email(body.email)?;
+    let pass_hash = hash_password(&body.password)?;
+    let email = validate_email(&body.email)?;
     let user = create_user(body.into_inner(), email, pass_hash);
     user.insert(&db).await?;
-    Ok(HttpResponse::Ok(user.response_user()))
+    Ok(HttpResponse::Ok().json(user.response_user()))
 }
 
 fn is_bot(url: &Option<String>, time: &Option<String>) -> bool {
     match url {
         Some(u) => {
-            if url != "https://" {return true}
+            if u != "https://" {return true}
         },
         None => {return true}
     }
@@ -72,9 +74,11 @@ fn valid_password(pass: &String, confirm_pass: &String) -> Result<(), AppError>
     if pass.chars().count() < 12 {
         return Err(AppError::invalid_input("Password must contain at least 12 characters"));
     }
+
+    Ok(())
 }
 
-fn validate_email(email: String) -> Result<String, AppError> {
+fn validate_email(email: &String) -> Result<String, AppError> {
     if !EmailAddress::is_valid(email) {
         return Err(AppError::invalid_input("Invalid email address"));
     }
@@ -82,12 +86,12 @@ fn validate_email(email: String) -> Result<String, AppError> {
     Ok(email.to_lowercase())
 }
 
-fn hash_password(pass: String) -> Result<String, AppError> {
+fn hash_password(pass: &String) -> Result<String, AppError> {
     let salt = SaltString::generate(&mut OsRng);
     let argon2 = Argon2::default();
     match argon2.hash_password(pass.as_bytes(), &salt) {
         Ok(h) => Ok(h.to_string()),
-        Err(_) => Err(AppError::InternalError)
+        Err(e) => Err(AppError::InternalError(e.to_string()))
     }
 }
 

+ 10 - 4
src/main.rs

@@ -9,9 +9,11 @@ mod controllers;
 mod models;
 
 #[actix_web::main]
-async fn main() -> std::io::Result<()> {
+async fn main() -> Result<(), AppError> {
     let app_env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string());
-    let pool = PgPool::connect("postgres://leemorgan@localhost:5432/homo").await?;
+    let pool = PgPool::connect("postgres://leemorgan:password123@localhost:5432/homo")
+        .await
+        .expect("Failed to start Postgres");
 
     HttpServer::new(move || {
         let cors = if app_env == "development" {
@@ -29,7 +31,7 @@ async fn main() -> std::io::Result<()> {
         App::new()
             .wrap(middleware::Compress::default())
             .wrap(cors)
-            .app_data(web::Data::new(pool))
+            .app_data(web::Data::new(pool.clone()))
             .app_data(
                 web::JsonConfig::default().error_handler(|err, _req| {
                     AppError::JsonDeserializationError(err.to_string()).into()
@@ -37,7 +39,11 @@ async fn main() -> std::io::Result<()> {
             )
             .configure(routes::user::config)
     })
-        .bind(("0.0.0.0", 8000))?
+        .bind(("0.0.0.0", 8000))
+        .expect("Failed to bind to port")
         .run()
         .await
+        .expect("Failed to start server");
+
+    Ok(())
 }

+ 21 - 11
src/models/user.rs

@@ -1,15 +1,19 @@
 use uuid::Uuid;
 use chrono::{DateTime, Utc};
+use sqlx::PgPool;
+use serde::Serialize;
+use crate::app_error::AppError;
 
 pub struct User {
-    id: Uuid,
-    token: Uuid,
-    name: String,
-    email: String,
-    pass_hash: String,
-    created_at: DateTime<Utc>
+    pub id: Uuid,
+    pub token: Uuid,
+    pub name: String,
+    pub email: String,
+    pub pass_hash: String,
+    pub created_at: DateTime<Utc>
 }
 
+#[derive(Serialize)]
 pub struct ResponseUser {
     id: String,
     name: String,
@@ -19,7 +23,7 @@ pub struct ResponseUser {
 
 impl User {
     pub async fn insert(&self, db: &PgPool) -> Result<(), AppError> {
-        sqlx::query!(
+        let result = sqlx::query!(
             r#"
             INSERT INTO users (id, token, name, email, pass_hash, created_at)
             VALUES($1, $2, $3, $4, $5, $6)
@@ -32,17 +36,23 @@ impl User {
             self.created_at
         )
             .execute(db)
-            .await?;
+            .await;
 
-        Ok(())
+        match result {
+            Ok(_) => Ok(()),
+            Err(sqlx::Error::Database(db_err)) if db_err.code().as_deref() == Some("23505") => {
+                Err(AppError::invalid_input("User with this email already exists"))
+            },
+            Err(e) => Err(AppError::Database(e.into()))
+        }
     }
 
-    pub fn response_user(&self) -> ResponseUser {
+    pub fn response_user(self) -> ResponseUser {
         ResponseUser {
             id: self.id.to_string(),
             name: self.name,
             email: self.email,
-            created_at: self.to_rfc3339()
+            created_at: self.created_at.to_rfc3339()
         }
     }
 }