Răsfoiți Sursa

Create route to create user

Lee Morgan 1 lună în urmă
părinte
comite
83233f0793
7 a modificat fișierele cu 269 adăugiri și 18 ștergeri
  1. 140 3
      Cargo.lock
  2. 4 1
      Cargo.toml
  3. 43 5
      src/controllers/user/create.rs
  4. 20 2
      src/http_error.rs
  5. 3 3
      src/logic/hash_password.rs
  6. 4 4
      src/logic/valid_password.rs
  7. 55 0
      src/models/user.rs

+ 140 - 3
Cargo.lock

@@ -251,6 +251,18 @@ version = "1.0.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
 
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures 0.2.17",
+ "password-hash",
+]
+
 [[package]]
 name = "atoi"
 version = "2.0.0"
@@ -287,6 +299,15 @@ dependencies = [
  "serde_core",
 ]
 
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest 0.10.7",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.4"
@@ -388,12 +409,15 @@ version = "0.1.0"
 dependencies = [
  "actix-cors",
  "actix-web",
+ "argon2",
  "chrono",
  "dotenvy",
  "serde",
+ "serde_json",
  "sqlx",
  "thiserror",
  "uuid",
+ "validator",
 ]
 
 [[package]]
@@ -405,6 +429,7 @@ dependencies = [
  "iana-time-zone",
  "js-sys",
  "num-traits",
+ "serde",
  "wasm-bindgen",
  "windows-link",
 ]
@@ -542,6 +567,41 @@ dependencies = [
  "hybrid-array",
 ]
 
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "der"
 version = "0.7.10"
@@ -1096,6 +1156,12 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
 
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "1.1.0"
@@ -1453,6 +1519,17 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
 [[package]]
 name = "pem-rfc7468"
 version = "0.7.0"
@@ -1541,6 +1618,28 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.106"
@@ -1710,7 +1809,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2157,6 +2256,12 @@ dependencies = [
  "unicode-properties",
 ]
 
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
 [[package]]
 name = "subtle"
 version = "2.6.1"
@@ -2192,10 +2297,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
 dependencies = [
  "fastrand",
- "getrandom 0.3.4",
+ "getrandom 0.4.2",
  "once_cell",
  "rustix",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2415,10 +2520,42 @@ version = "1.23.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
 dependencies = [
+ "getrandom 0.4.2",
  "js-sys",
+ "serde_core",
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "validator"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
+dependencies = [
+ "idna",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+ "validator_derive",
+]
+
+[[package]]
+name = "validator_derive"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
+dependencies = [
+ "darling",
+ "once_cell",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"

+ 4 - 1
Cargo.toml

@@ -6,9 +6,11 @@ edition = "2024"
 [dependencies]
 actix-cors = "0.7.1"
 actix-web = "4.13.0"
-chrono = { version = "0.4.44" features = ["serde"]}
+argon2 = "0.5.3"
+chrono = { version = "0.4.44", features = ["serde"]}
 dotenvy = "0.15.7"
 serde = { version = "1.0.228", features = ["derive"]}
+serde_json = "1.0.150"
 sqlx = { version = "0.8.6", features = [
     "postgres",
     "runtime-tokio",
@@ -18,3 +20,4 @@ sqlx = { version = "0.8.6", features = [
 ]}
 thiserror = "2.0.18"
 uuid = { version = "1.23.1", features = ["v4", "v7", "serde"]}
+validator = { version = "0.20.0", features = ["derive"]}

+ 43 - 5
src/controllers/user/create.rs

@@ -1,21 +1,59 @@
 use actix_web::{HttpResponse, web, post};
 use serde::Deserialize;
+use serde_json::json;
+use validator::Validate;
+use sqlx::PgPool;
+use chrono::{DateTime, Utc, Duration};
 use crate::{
-    http_error::HttpError
+    http_error::HttpError,
+    logic::{valid_password, hash_password},
+    models::user::User
 };
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Validate)]
 struct Body {
+    #[validate(email)]
     name: String,
     email: String,
     password: String,
-    confirm_password: String
+    confirm_password: String,
+    url: Option<String>,
+    time: Option<String>
 }
 
 #[post("/user")]
 pub async fn route(
-    body: web::Json<Body>
+    db: web::Data<PgPool>,
+    web::Json(body): web::Json<Body>
 ) -> Result<HttpResponse, HttpError> {
+    if is_bot(&body.url, &body.time) {
+        return Ok(HttpResponse::Ok().json(json!({"success": true})));
+    }
+    valid_password(&body.password, &body.confirm_password)?;
+    let pass_hash = hash_password(&body.password)?;
+    let user = User::new(body.name, body.email, pass_hash);
+    user.insert_one(&db).await?;
+    Ok(HttpResponse::Ok().json(user.response()))
+}
+
+fn is_bot(url: &Option<String>, time: &Option<String>) -> bool {
+    match url {
+        Some(u) => {
+            if u != "https://" {return true}
+        },
+        None => {return true}
+    }
+
+    let body_time = match time {
+        Some(t) => match t.parse::<DateTime<Utc>>() {
+            Ok(u) => u,
+            Err(_) => {return true}
+        }
+        None => {return true}
+    };
+
+    let now = Utc::now();
+    let age = now.signed_duration_since(body_time);
 
-    Ok(HttpResponse::Ok().body("Test"))
+    age > Duration::minutes(10)
 }

+ 20 - 2
src/http_error.rs

@@ -16,17 +16,35 @@ struct ErrorInfo {
 #[derive(Error, Debug)]
 pub enum HttpError {
     #[error("{0}")]
-    JsonDeserializationError(String)
+    JsonDeserializationError(String),
+
+    #[error("Internal Server Error")]
+    Database(#[from] sqlx::Error),
+
+    #[error("{0}")]
+    InvalidInput(String),
+
+    #[error("Internal Server Error")]
+    InternalError(String)
 }
 
 impl ResponseError for HttpError {
     fn status_code(&self) -> StatusCode {
         match self {
-            HttpError::JsonDeserializationError(_) => StatusCode::BAD_REQUEST
+            HttpError::JsonDeserializationError(_) => StatusCode::BAD_REQUEST,
+            HttpError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
+            HttpError::InvalidInput(_) => StatusCode::BAD_REQUEST,
+            HttpError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR
         }
     }
 
     fn error_response(&self) -> HttpResponse {
+        match self {
+            HttpError::Database(s) => {eprintln!("Database error: {}", s)},
+            HttpError::InternalError(s) => {eprintln!("Internal Error: {}", s)}
+            _ => ()
+        };
+
         let body = ErrorBody {
             error: ErrorInfo {
                 code: self.status_code().as_u16(),

+ 3 - 3
src/logic/hash_password.rs

@@ -6,13 +6,13 @@ use argon2::{
         rand_core::OsRng
     }
 };
-use crate::app_error::AppError;
+use crate::http_error::HttpError;
 
-pub fn hash_password(pass: &String) -> Result<String, AppError> {
+pub fn hash_password(pass: &String) -> Result<String, HttpError> {
     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(e) => Err(AppError::InternalError(e.to_string()))
+        Err(e) => Err(HttpError::InternalError(e.to_string()))
     }
 }

+ 4 - 4
src/logic/valid_password.rs

@@ -1,12 +1,12 @@
-use crate::app_error::AppError;
+use crate::http_error::HttpError;
 
-pub fn valid_password(pass: &String, confirm_pass: &String) -> Result<(), AppError> {
+pub fn valid_password(pass: &String, confirm_pass: &String) -> Result<(), HttpError> {
     if *pass != *confirm_pass {
-        return Err(AppError::invalid_input("Passwords do not match"));
+        return Err(HttpError::InvalidInput("Passwords do not match".into()));
     }
 
     if pass.chars().count() < 12 {
-        return Err(AppError::invalid_input("Password must contain at least 12 characters"));
+        return Err(HttpError::InvalidInput("Password must contain at least 12 characters".into()));
     }
 
     Ok(())

+ 55 - 0
src/models/user.rs

@@ -1,10 +1,65 @@
 use uuid::Uuid;
 use chrono::{DateTime, Utc};
+use sqlx::PgPool;
+use serde::Serialize;
+use crate::http_error::HttpError;
 
 pub struct User {
     id: Uuid,
     token: Uuid,
     name: String,
+    email: String,
     pass_hash: String,
     created_at: DateTime<Utc>
 }
+
+#[derive(Serialize)]
+pub struct ResponseUser {
+    id: String,
+    name: String,
+    email: String
+}
+
+impl User {
+    pub fn new(name: String, email: String, pass_hash: String) -> Self {
+        Self {
+            id: Uuid::now_v7(),
+            token: Uuid::new_v4(),
+            name: name,
+            email: email.to_lowercase(),
+            pass_hash: pass_hash,
+            created_at: Utc::now()
+        }
+    }
+
+    pub async fn insert_one(&self, db: &PgPool) -> Result<(), HttpError> {
+        let result = sqlx::query(
+            "INSERT into users(id, token, name, email, pass_hash, created_at),
+            VALUES($1, $2, $3, $4, $5, $6)"
+        )
+            .bind(&self.id)
+            .bind(&self.token)
+            .bind(&self.name)
+            .bind(&self.email)
+            .bind(&self.pass_hash)
+            .bind(&self.created_at)
+            .execute(db)
+            .await;
+
+        match result {
+            Ok(_) => Ok(()),
+            Err(sqlx::Error::Database(db_err)) if db_err.code().as_deref() == Some("23505") => {
+                Err(HttpError::InvalidInput("User with this email already exists".into()))
+            },
+            Err(e) => Err(HttpError::Database(e.into()))
+        }
+    }
+
+    pub fn response(self) -> ResponseUser {
+        ResponseUser {
+            id: self.id.to_string(),
+            name: self.name,
+            email: self.email
+        }
+    }
+}