Jelajahi Sumber

Create docs for the user login route

Lee Morgan 1 bulan lalu
induk
melakukan
14691f7bac
9 mengubah file dengan 177 tambahan dan 5 penghapusan
  1. 64 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 3 0
      docs/openapi.yaml
  4. 41 0
      docs/paths/user/login.yaml
  5. 0 0
      docs/redoc-static.html
  6. 27 0
      src/auth.rs
  7. 14 2
      src/http_error.rs
  8. 5 1
      src/main.rs
  9. 22 2
      src/models/user.rs

+ 64 - 0
Cargo.lock

@@ -412,6 +412,7 @@ dependencies = [
  "argon2",
  "chrono",
  "dotenvy",
+ "jsonwebtoken",
  "serde",
  "serde_json",
  "sqlx",
@@ -1229,6 +1230,23 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "jsonwebtoken"
+version = "10.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
+dependencies = [
+ "base64",
+ "getrandom 0.2.17",
+ "js-sys",
+ "pem",
+ "serde",
+ "serde_json",
+ "signature",
+ "simple_asn1",
+ "zeroize",
+]
+
 [[package]]
 name = "language-tags"
 version = "0.3.2"
@@ -1389,6 +1407,16 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-bigint-dig"
 version = "0.8.6"
@@ -1530,6 +1558,16 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64",
+ "serde_core",
+]
+
 [[package]]
 name = "pem-rfc7468"
 version = "0.7.0"
@@ -1988,6 +2026,18 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
 
+[[package]]
+name = "simple_asn1"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "thiserror",
+ "time",
+]
+
 [[package]]
 name = "slab"
 version = "0.4.12"
@@ -3063,6 +3113,20 @@ 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 - 0
Cargo.toml

@@ -9,6 +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"
 serde = { version = "1.0.228", features = ["derive"]}
 serde_json = "1.0.150"
 sqlx = { version = "0.8.6", features = [

+ 3 - 0
docs/openapi.yaml

@@ -34,3 +34,6 @@ paths:
   /user:
     post:
       $ref: "./paths/user/create.yaml"
+  /user/login:
+    post:
+      $ref: "./paths/user/login.yaml"

+ 41 - 0
docs/paths/user/login.yaml

@@ -0,0 +1,41 @@
+operationId: userLogin
+summary: Login
+security: []
+description: User login
+tags: [User]
+requestBody:
+  content:
+    application/json:
+      schema:
+        type: object
+        required:
+          - email
+          - password
+        properties:
+          email:
+            type: string
+            format: email
+            description: User email
+            example: john.smith@mail.com
+          password:
+            type: string
+            format: password
+            description: User password
+            example: password123
+
+responses:
+  "200":
+    description: Login successful. Returns success object. Sets 'user' cookie.
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            success:
+              type: boolean
+  "400":
+    $ref: "#/components/responses/400"
+  "401":
+    $ref: "#/components/responses/401"
+  "500":
+    $ref: "#/components/responses/500"

File diff ditekan karena terlalu besar
+ 0 - 0
docs/redoc-static.html


+ 27 - 0
src/auth.rs

@@ -0,0 +1,27 @@
+use actix_web::HttpRequest;
+use sqlx::PgPool;
+use uuid::Uuid;
+use jsonwebtoken::{decode, DecodingKey, Validation};
+use serde::Deserialize;
+use crate::{
+    models::user::User,
+    http_error::HttpError,
+    JWT_SECRET
+};
+
+#[derive(Deserialize)]
+struct UserJwt{
+    id: Uuid,
+    session_token: Uuid
+}
+
+pub async fn user_auth(db: &PgPool, req: HttpRequest) -> Result<User, HttpError> {
+    let cookie = req.cookie("user").ok_or(HttpError::Auth)?;
+    let user_data = decode::<UserJwt>(
+        &cookie.value(),
+        &DecodingKey::from_secret(JWT_SECRET.get().unwrap().as_bytes()),
+        &Validation::default()
+    )?.claims;
+    let user = User::find_one_by_session(db, user_data.id, user_data.session_token).await?;
+    Ok(user)
+}

+ 14 - 2
src/http_error.rs

@@ -25,7 +25,16 @@ pub enum HttpError {
     InvalidInput(String),
 
     #[error("Internal Server Error")]
-    InternalError(String)
+    InternalError(String),
+
+    #[error("Unauthorized")]
+    Auth,
+
+    #[error("Invalid UUID")]
+    InvalidUuid(#[from] uuid::Error),
+
+    #[error("Unauthorized")]
+    InvalidJwt(#[from] jsonwebtoken::errors::Error)
 }
 
 impl ResponseError for HttpError {
@@ -34,7 +43,10 @@ impl ResponseError for HttpError {
             HttpError::JsonDeserializationError(_) => StatusCode::BAD_REQUEST,
             HttpError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
             HttpError::InvalidInput(_) => StatusCode::BAD_REQUEST,
-            HttpError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR
+            HttpError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
+            HttpError::Auth => StatusCode::UNAUTHORIZED,
+            HttpError::InvalidUuid(_) => StatusCode::BAD_REQUEST,
+            HttpError::InvalidJwt(_) => StatusCode::UNAUTHORIZED
         }
     }
 

+ 5 - 1
src/main.rs

@@ -2,13 +2,16 @@ use actix_web::{HttpServer, web, App, middleware};
 use actix_cors::Cors;
 use http_error::HttpError;
 use sqlx::PgPool;
-use std::env;
+use std::{env, sync::OnceLock};
 
 mod http_error;
 mod routes;
 mod controllers;
 mod models;
 mod logic;
+mod auth;
+
+static JWT_SECRET: OnceLock<String> = OnceLock::new();
 
 #[actix_web::main]
 async fn main() -> std::io::Result<()> {
@@ -18,6 +21,7 @@ async fn main() -> std::io::Result<()> {
     let port: u16 = env_var("PORT");
     let app_env: String = env_var("APP_ENV");
     let database_url: String = env_var("DATABASE_URL");
+    JWT_SECRET.set(env_var("JWT_SECRET")).expect("Failed to set JWT");
 
     //Database
     let pool = PgPool::connect(&database_url)

+ 22 - 2
src/models/user.rs

@@ -1,16 +1,17 @@
 use uuid::Uuid;
 use chrono::{DateTime, Utc};
-use sqlx::PgPool;
+use sqlx::{PgPool, FromRow};
 use serde::Serialize;
 use crate::http_error::HttpError;
 
+#[derive(FromRow)]
 pub struct User {
     id: Uuid,
     session_token: Uuid,
     name: String,
     email: String,
     pass_hash: String,
-    tokens: i64,
+    tokens: u64,
     created_at: DateTime<Utc>
 }
 
@@ -58,6 +59,25 @@ impl User {
         }
     }
 
+    pub async fn find_one_by_session(
+        db: &PgPool,
+        user_id: Uuid,
+        session_token: Uuid
+    ) -> Result<User, HttpError> {
+        let user = sqlx::query_as::<_, User>(
+            "SELECT *
+            FROM users
+            WHERE id = $1
+                and session_token = $2"
+        )
+            .bind(user_id)
+            .bind(session_token)
+            .fetch_one(db)
+            .await?;
+
+        Ok(user)
+    }
+
     pub fn response(self) -> ResponseUser {
         ResponseUser {
             id: self.id.to_string(),

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini