Lee Morgan 2 hónapja
szülő
commit
64df814b1e

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@
 *.swp
 *.swo
 redoc-static.html
+.env

+ 19 - 0
.sqlx/query-6f9bc679202420123c9549513241acfe289185a372ac631a4a6ea45d2ba5a9bc.json

@@ -0,0 +1,19 @@
+{
+  "db_name": "PostgreSQL",
+  "query": "\n            INSERT INTO users (id, token, name, email, pass_hash, created_at)\n            VALUES($1, $2, $3, $4, $5, $6)\n            ",
+  "describe": {
+    "columns": [],
+    "parameters": {
+      "Left": [
+        "Uuid",
+        "Uuid",
+        "Text",
+        "Text",
+        "Text",
+        "Timestamptz"
+      ]
+    },
+    "nullable": []
+  },
+  "hash": "6f9bc679202420123c9549513241acfe289185a372ac631a4a6ea45d2ba5a9bc"
+}

+ 239 - 0
Cargo.lock

@@ -19,6 +19,21 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "actix-cors"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
+dependencies = [
+ "actix-utils",
+ "actix-web",
+ "derive_more",
+ "futures-util",
+ "log",
+ "once_cell",
+ "smallvec",
+]
+
 [[package]]
 name = "actix-http"
 version = "3.12.1"
@@ -236,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"
@@ -272,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"
@@ -421,6 +457,16 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
@@ -595,6 +641,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "email_address"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.35"
@@ -642,6 +697,12 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
 [[package]]
 name = "find-msvc-tools"
 version = "0.1.9"
@@ -681,6 +742,21 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
 
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.2"
@@ -896,8 +972,11 @@ dependencies = [
 name = "homo"
 version = "0.1.0"
 dependencies = [
+ "actix-cors",
  "actix-web",
+ "argon2",
  "chrono",
+ "email_address",
  "serde",
  "serde_json",
  "sqlx",
@@ -1171,6 +1250,12 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
 [[package]]
 name = "litemap"
 version = "0.8.2"
@@ -1253,6 +1338,23 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "num-bigint-dig"
 version = "0.8.6"
@@ -1311,6 +1413,50 @@ version = "1.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
 
+[[package]]
+name = "openssl"
+version = "0.10.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "parking"
 version = "2.2.1"
@@ -1340,6 +1486,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"
@@ -1587,6 +1744,19 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "rustversion"
 version = "1.0.22"
@@ -1599,12 +1769,44 @@ version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
 
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.28"
@@ -1806,6 +2008,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
 dependencies = [
  "base64",
  "bytes",
+ "chrono",
  "crc",
  "crossbeam-queue",
  "either",
@@ -1819,6 +2022,7 @@ dependencies = [
  "indexmap",
  "log",
  "memchr",
+ "native-tls",
  "once_cell",
  "percent-encoding",
  "serde",
@@ -1826,8 +2030,11 @@ dependencies = [
  "sha2",
  "smallvec",
  "thiserror",
+ "tokio",
+ "tokio-stream",
  "tracing",
  "url",
+ "uuid",
 ]
 
 [[package]]
@@ -1864,6 +2071,7 @@ dependencies = [
  "sqlx-postgres",
  "sqlx-sqlite",
  "syn",
+ "tokio",
  "url",
 ]
 
@@ -1878,6 +2086,7 @@ dependencies = [
  "bitflags",
  "byteorder",
  "bytes",
+ "chrono",
  "crc",
  "digest 0.10.7",
  "dotenvy",
@@ -1906,6 +2115,7 @@ dependencies = [
  "stringprep",
  "thiserror",
  "tracing",
+ "uuid",
  "whoami",
 ]
 
@@ -1919,6 +2129,7 @@ dependencies = [
  "base64",
  "bitflags",
  "byteorder",
+ "chrono",
  "crc",
  "dotenvy",
  "etcetera",
@@ -1943,6 +2154,7 @@ dependencies = [
  "stringprep",
  "thiserror",
  "tracing",
+ "uuid",
  "whoami",
 ]
 
@@ -1953,6 +2165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
 dependencies = [
  "atoi",
+ "chrono",
  "flume",
  "futures-channel",
  "futures-core",
@@ -1968,6 +2181,7 @@ dependencies = [
  "thiserror",
  "tracing",
  "url",
+ "uuid",
 ]
 
 [[package]]
@@ -2015,6 +2229,19 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "thiserror"
 version = "2.0.18"
@@ -2107,6 +2334,17 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-util"
 version = "0.7.18"
@@ -2221,6 +2459,7 @@ version = "1.23.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
 dependencies = [
+ "getrandom 0.4.2",
  "js-sys",
  "wasm-bindgen",
 ]

+ 11 - 2
Cargo.toml

@@ -4,10 +4,19 @@ version = "0.1.0"
 edition = "2024"
 
 [dependencies]
+actix-cors = "0.7.1"
 actix-web = "4.13.0"
+argon2 = "0.5.3"
 chrono = "0.4.44"
+email_address = "0.2.9"
 serde = "1.0.228"
 serde_json = "1.0.149"
-sqlx = "0.8.6"
+sqlx = { version = "0.8.6", features = [
+    "postgres",
+    "runtime-tokio",
+    "tls-native-tls",
+    "chrono",
+    "uuid"
+]}
 thiserror = "2.0.18"
-uuid = "1.23.1"
+uuid = { version="1.23.1", features=["v7"] }

+ 16 - 5
src/app_error.rs

@@ -1,5 +1,5 @@
 use actix_web::{HttpResponse, ResponseError, http::StatusCode};
-use thiserror::ErrorInfo;
+use thiserror::Error;
 use serde::Serialize;
 
 #[derive(Serialize)]
@@ -16,20 +16,25 @@ struct ErrorInfo {
 #[derive(Error)]
 pub enum AppError {
     #[error("Internal Server Error")]
-    InternalError(String)
+    InternalError(String),
+
+    #[error("{0}")]
+    InvalidInput(String),
 }
 
 impl ResponseError for AppError {
     fn status_code(&self) -> StatusCode {
         match self {
-            AppError::InternalError => StatusCode::INTERNAL_SERVER_ERROR
+            AppError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
+            AppError::InvalidInput(_) => StatusCode::BAD_REQUEST
         }
     }
 
     fn error_response(&self) -> HttpResponse {
         match self {
-            AppError::InternalError(s) => {eprintln!("Internal Error: {}", s)}
-        }
+            AppError::InternalError(s) => {eprintln!("Internal Error: {}", s)},
+            _ => ()
+        };
 
         let body = ErrorBody {
             error: ErrorInfo {
@@ -41,3 +46,9 @@ impl ResponseError for AppError {
         HttpResponse::build(self.status_code()).json(body)
     }
 }
+
+impl AppError {
+    pub fn invalid_input(msg: &str) -> Self {
+        AppError::InvalidInput(msg.to_owned())
+    }
+}

+ 70 - 7
src/controllers/user/create.rs

@@ -1,9 +1,20 @@
 use actix_web::{HttpResponse, web, post};
 use serde::Serialize;
 use serde_json::json;
-use sqlx::{PgPool, types::Uuid};
+use sqlx::PgPool;
+use email_address::EmailAddress;
+use uuid::Uuid;
+use argon2::{
+    Argon2,
+    password_hash::{
+        SaltString,
+        PasswordHasher,
+        rand_core::OsRng
+    }
+};
 use crate::{
-    app_error::AppError
+    app_error::AppError,
+    models::user::User
 };
 
 #[derive(Serialize)]
@@ -13,17 +24,22 @@ struct Body {
     password: String,
     confirm_password: String,
     url: Option<String>,
-    time: Option<String>
+    time: Option<String>,
 }
 
 pub async fn route(
     db: web::Data<PgPool>,
     body: web::Json<Body>
 ) -> Result<HttpResponse, AppError> {
-    let email = body.email.to_lowercase();
     if is_bot(&body.url, &body.time) {
-        return Ok(HttpResponse::Ok().json(json!("success": true))));
+        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 user = create_user(body.into_inner(), email, pass_hash);
+    user.insert(&db).await?;
+    Ok(HttpResponse::Ok(user.response_user()))
 }
 
 fn is_bot(url: &Option<String>, time: &Option<String>) -> bool {
@@ -34,7 +50,54 @@ fn is_bot(url: &Option<String>, time: &Option<String>) -> bool {
         None => {return true}
     }
 
-    match time {
-        Some(t) => match DateTime::
+    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);
+
+    age > Duration::minutes(10)
+}
+
+fn valid_password(pass: &String, confirm_pass: &String) -> Result<(), AppError> {
+    if *pass != *confirm_pass {
+        return Err(AppError::invalid_input("Passwords do not match"));
+    }
+
+    if pass.chars().count() < 12 {
+        return Err(AppError::invalid_input("Password must contain at least 12 characters"));
+    }
+}
+
+fn validate_email(email: String) -> Result<String, AppError> {
+    if !EmailAddress::is_valid(email) {
+        return Err(AppError::invalid_input("Invalid email address"));
+    }
+
+    Ok(email.to_lowercase())
+}
+
+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)
+    }
+}
+
+fn create_user(body: Body, email: String, pass_hash: String) -> User {
+    User {
+        id: Uuid::now_v7(),
+        token: Uuid::new_v4(),
+        name: body.name,
+        email: email,
+        pass_hash: pass_hash,
+        created_at: Utc::now()
     }
 }

+ 1 - 0
src/main.rs

@@ -6,6 +6,7 @@ use crate::app_error::AppError;
 mod app_error;
 mod routes;
 mod controllers;
+mod models;
 
 #[actix_web::main]
 async fn main() -> std::io::Result<()> {

+ 18 - 2
src/models/user.rs

@@ -3,15 +3,22 @@ use chrono::{DateTime, Utc};
 
 pub struct User {
     id: Uuid,
-    token: String,
+    token: Uuid,
     name: String,
     email: String,
     pass_hash: String,
     created_at: DateTime<Utc>
 }
 
+pub struct ResponseUser {
+    id: String,
+    name: String,
+    email: String,
+    created_at: String
+}
+
 impl User {
-    pub fn insert(&self, &db: &PgPool) -> Result<(), AppError> {
+    pub async fn insert(&self, db: &PgPool) -> Result<(), AppError> {
         sqlx::query!(
             r#"
             INSERT INTO users (id, token, name, email, pass_hash, created_at)
@@ -29,4 +36,13 @@ impl User {
 
         Ok(())
     }
+
+    pub fn response_user(&self) -> ResponseUser {
+        ResponseUser {
+            id: self.id.to_string(),
+            name: self.name,
+            email: self.email,
+            created_at: self.to_rfc3339()
+        }
+    }
 }