Parcourir la source

Create the login route for users

Lee Morgan il y a 1 mois
Parent
commit
c4d0dbe3a4

+ 25 - 19
Cargo.lock

@@ -899,8 +899,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "wasi",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -1232,19 +1234,17 @@ dependencies = [
 
 [[package]]
 name = "jsonwebtoken"
-version = "10.4.0"
+version = "9.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
+checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
 dependencies = [
  "base64",
- "getrandom 0.2.17",
  "js-sys",
  "pem",
+ "ring",
  "serde",
  "serde_json",
- "signature",
  "simple_asn1",
- "zeroize",
 ]
 
 [[package]]
@@ -1808,6 +1808,20 @@ version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
 
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "rsa"
 version = "0.9.10"
@@ -2546,6 +2560,12 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
 [[package]]
 name = "url"
 version = "2.5.8"
@@ -3113,20 +3133,6 @@ name = "zeroize"
 version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
-dependencies = [
- "zeroize_derive",
-]
-
-[[package]]
-name = "zeroize_derive"
-version = "1.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
 
 [[package]]
 name = "zerotrie"

+ 1 - 1
Cargo.toml

@@ -9,7 +9,7 @@ actix-web = "4.13.0"
 argon2 = "0.5.3"
 chrono = { version = "0.4.44", features = ["serde"]}
 dotenvy = "0.15.7"
-jsonwebtoken = "10.4.0"
+jsonwebtoken = "9"
 serde = { version = "1.0.228", features = ["derive"]}
 serde_json = "1.0.150"
 sqlx = { version = "0.8.6", features = [

+ 57 - 0
src/controllers/user/login.rs

@@ -0,0 +1,57 @@
+use actix_web::{HttpResponse, web, post};
+use serde::{Serialize, Deserialize};
+use sqlx::PgPool;
+use jsonwebtoken::{encode, Header, EncodingKey};
+use std::time::{SystemTime, UNIX_EPOCH};
+use serde_json::json;
+use uuid::Uuid;
+use crate::{
+    http_error::HttpError,
+    models::user::User,
+    logic::{compare_password, create_cookie},
+    JWT_SECRET
+};
+
+#[derive(Deserialize)]
+struct Body {
+    email: String,
+    password: String
+}
+
+#[derive(Serialize)]
+struct Claims {
+    id: Uuid,
+    session_token: Uuid,
+    exp: usize
+}
+
+#[post("/user/login")]
+pub async fn route(
+    db: web::Data<PgPool>,
+    web::Json(body): web::Json<Body>
+) -> Result<HttpResponse, HttpError> {
+    let user = User::find_one_by_email(&db, body.email).await?;
+    compare_password(&body.password, &user.pass_hash)?;
+    let jwt = create_jwt(user.id, user.session_token)?;
+    let cookie = create_cookie("user".to_string(), jwt);
+    Ok(HttpResponse::Ok().cookie(cookie).json(json!({"success": true})))
+}
+
+fn create_jwt(user_id: Uuid, user_token: Uuid) -> Result<String, HttpError> {
+    let claims = Claims {
+        id: user_id,
+        session_token: user_token,
+        exp: SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .unwrap()
+            .as_secs() as usize + (90 * 24 * 60 * 60)
+    };
+
+    let token = encode(
+        &Header::default(),
+        &claims,
+        &EncodingKey::from_secret(JWT_SECRET.get().unwrap().as_bytes())
+    )?;
+
+    Ok(token)
+}

+ 1 - 0
src/controllers/user/mod.rs

@@ -1 +1,2 @@
 pub mod create;
+pub mod login;

+ 6 - 2
src/http_error.rs

@@ -34,7 +34,10 @@ pub enum HttpError {
     InvalidUuid(#[from] uuid::Error),
 
     #[error("Unauthorized")]
-    InvalidJwt(#[from] jsonwebtoken::errors::Error)
+    InvalidJwt(#[from] jsonwebtoken::errors::Error),
+
+    #[error("{0}")]
+    NotFound(String)
 }
 
 impl ResponseError for HttpError {
@@ -46,7 +49,8 @@ impl ResponseError for HttpError {
             HttpError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
             HttpError::Auth => StatusCode::UNAUTHORIZED,
             HttpError::InvalidUuid(_) => StatusCode::BAD_REQUEST,
-            HttpError::InvalidJwt(_) => StatusCode::UNAUTHORIZED
+            HttpError::InvalidJwt(_) => StatusCode::UNAUTHORIZED,
+            HttpError::NotFound(_) => StatusCode::NOT_FOUND
         }
     }
 

+ 17 - 0
src/logic/compare_password.rs

@@ -0,0 +1,17 @@
+use argon2::{
+    Argon2,
+    password_hash::{
+        PasswordHash,
+        PasswordVerifier
+    }
+};
+use crate::http_error::HttpError;
+
+pub fn compare_password(password: &String, hash: &String) -> Result<(), HttpError> {
+    let parsed_hash = PasswordHash::new(hash)
+        .map_err(|_| HttpError::InternalError("Parsing hash failed while comparing pass".to_string()))?;
+
+    Argon2::default()
+        .verify_password(password.as_bytes(), &parsed_hash)
+        .map_err(|_| HttpError::Auth)
+}

+ 22 - 0
src/logic/create_cookie.rs

@@ -0,0 +1,22 @@
+use actix_web::cookie::{Cookie, SameSite, time::Duration};
+
+pub fn create_cookie(name: String, value: String) -> Cookie<'static> {
+    if cfg!(debug_assertions){
+        Cookie::build(name, value)
+            .path("/")
+            .http_only(true)
+            .same_site(SameSite::Lax)
+            .secure(false)
+            .max_age(Duration::days(90))
+            .finish()
+    } else {
+        Cookie::build(name, value)
+            .domain("exmaple.com")
+            .path("/")
+            .http_only(true)
+            .same_site(SameSite::Lax)
+            .secure(true)
+            .max_age(Duration::days(90))
+            .finish()
+    }
+}

+ 4 - 0
src/logic/mod.rs

@@ -1,5 +1,9 @@
 pub mod valid_password;
 pub mod hash_password;
+pub mod compare_password;
+pub mod create_cookie;
 
 pub use valid_password::valid_password;
 pub use hash_password::hash_password;
+pub use compare_password::compare_password;
+pub use create_cookie::create_cookie;

+ 25 - 7
src/models/user.rs

@@ -6,13 +6,13 @@ use crate::http_error::HttpError;
 
 #[derive(FromRow)]
 pub struct User {
-    id: Uuid,
-    session_token: Uuid,
-    name: String,
-    email: String,
-    pass_hash: String,
-    tokens: u64,
-    created_at: DateTime<Utc>
+    pub id: Uuid,
+    pub session_token: Uuid,
+    pub name: String,
+    pub email: String,
+    pub pass_hash: String,
+    pub tokens: i64,
+    pub created_at: DateTime<Utc>
 }
 
 #[derive(Serialize)]
@@ -78,6 +78,24 @@ impl User {
         Ok(user)
     }
 
+    pub async fn find_one_by_email(db: &PgPool, user_email: String) -> Result<User, HttpError> {
+        let email = user_email.to_lowercase();
+
+        let user = sqlx::query_as::<_, User>(
+            "SELECT *
+            FROM users
+            WHERE email = $1"
+        )
+            .bind(email)
+            .fetch_optional(db)
+            .await?;
+
+        match user {
+            Some(u) => Ok(u),
+            _ => Err(HttpError::NotFound("Invalid email/password combination".into()))
+        }
+    }
+
     pub fn response(self) -> ResponseUser {
         ResponseUser {
             id: self.id.to_string(),

+ 3 - 1
src/routes/user.rs

@@ -1,8 +1,10 @@
 use actix_web::web;
 use crate::controllers::user::{
-    create
+    create,
+    login
 };
 
 pub fn config(cfg: &mut web::ServiceConfig) {
     cfg.service(create::route);
+    cfg.service(login::route);
 }