ソースを参照

Create user login route

Lee Morgan 2 ヶ月 前
コミット
d6636f1401

+ 1 - 0
.gitignore

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

+ 1 - 1
sql/users.sql

@@ -4,6 +4,6 @@ CREATE TABLE IF NOT EXISTS users (
     token UUID NOT NULL,
     name TEXT NOT NULL,
     email TEXT NOT NULL UNIQUE,
-    pass_hash TEXT,
+    pass_hash TEXT NOT NULL,
     created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
 )

+ 14 - 2
src/app_error.rs

@@ -25,7 +25,13 @@ pub enum AppError {
     Database(#[from] sqlx::Error),
 
     #[error("{0}")]
-    JsonDeserializationError(String)
+    JsonDeserializationError(String),
+
+    #[error("{0}")]
+    NotFound(String),
+
+    #[error("Unauthorized")]
+    Auth,
 }
 
 impl ResponseError for AppError {
@@ -34,7 +40,9 @@ impl ResponseError for AppError {
             AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
             AppError::InvalidInput(_) => StatusCode::BAD_REQUEST,
             AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
-            AppError::JsonDeserializationError(_) => StatusCode::BAD_REQUEST
+            AppError::JsonDeserializationError(_) => StatusCode::BAD_REQUEST,
+            AppError::NotFound(_) => StatusCode::NOT_FOUND,
+            AppError::Auth => StatusCode::UNAUTHORIZED
         }
     }
 
@@ -60,4 +68,8 @@ impl AppError {
     pub fn invalid_input(msg: &str) -> Self {
         AppError::InvalidInput(msg.into())
     }
+
+    pub fn not_found(msg: &str) -> Self {
+        AppError::NotFound(msg.into())
+    }
 }

+ 1 - 1
src/controllers/user/create.rs

@@ -41,7 +41,7 @@ pub async fn route(
     let email = validate_email(&body.email)?;
     let user = create_user(body.into_inner(), email, pass_hash);
     user.insert(&db).await?;
-    Ok(HttpResponse::Ok().json(user.response_user()))
+    Ok(HttpResponse::Ok().json(json!({"success": true})))
 }
 
 fn is_bot(url: &Option<String>, time: &Option<String>) -> bool {

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

@@ -0,0 +1,43 @@
+use actix_web::{HttpResponse, web, post};
+use serde::Deserialize;
+use serde_json::json;
+use sqlx::PgPool;
+use argon2::{
+    Argon2,
+    password_hash::{
+        PasswordHash,
+        PasswordVerifier
+    }
+};
+use crate::{
+    app_error::AppError,
+    models::user::User,
+    logic::create_cookie
+};
+
+#[derive(Deserialize)]
+struct Body {
+    email: String,
+    password: String
+}
+
+#[post("/user/login")]
+pub async fn route(
+    db: web::Data<PgPool>,
+    body: web::Json<Body>
+) -> Result<HttpResponse, AppError> {
+    let email = body.email.to_lowercase();
+    let user = User::find_one_by_email(&db, &email).await?;
+    compare_password(&body.password, &user.pass_hash)?;
+    let cookie = create_cookie("user".to_string(), user.id.to_string());
+    Ok(HttpResponse::Ok().cookie(cookie).json(json!({"success": true})))
+}
+
+fn compare_password(pass: &String, hash: &String) -> Result<(), AppError> {
+    let parsed_hash = PasswordHash::new(hash)
+        .map_err(|e| AppError::InternalError(e.to_string()))?;
+
+    Argon2::default()
+        .verify_password(pass.as_bytes(), &parsed_hash)
+        .map_err(|_| AppError::Auth)
+}

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

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

+ 18 - 0
src/logic/create_cookie.rs

@@ -0,0 +1,18 @@
+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)
+            .finish()
+    } else {
+        Cookie::build(name, value)
+            .domain("home.leemorgan.dev")
+            .path("/")
+            .http_only(true)
+            .same_site(SameSite::Lax)
+            .max_age(Duration::days(90))
+            .finish()
+    }
+}

+ 3 - 0
src/logic/mod.rs

@@ -0,0 +1,3 @@
+pub mod create_cookie;
+
+pub use create_cookie::create_cookie;

+ 1 - 0
src/main.rs

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

+ 17 - 0
src/models/user.rs

@@ -47,6 +47,23 @@ impl User {
         }
     }
 
+    pub async fn find_one_by_email(db: &PgPool, email: &String) -> Result<User, AppError> {
+        let user = sqlx::query_as!(
+           User,
+           "SELECT *
+           FROM users
+           WHERE email = $1",
+           email
+        )
+            .fetch_optional(db)
+            .await?;
+
+        match user {
+            Some(u) => Ok(u),
+            None => Err(AppError::not_found("User with that email does not exist'"))
+        }
+    }
+
     pub fn response_user(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);
 }