From 905baba00acf7e2ace880a46b435cac22ddcf8bd Mon Sep 17 00:00:00 2001
From: Franek <me@sador.me>
Date: Sun, 20 Apr 2025 20:29:46 +0200
Subject: [PATCH] add(v1): PUT /posts

---
 src/database/models/post.rs |  9 +++---
 src/database/models/user.rs |  8 ++---
 src/utils/jwt.rs            | 14 ++++-----
 src/utils/mod.rs            |  3 +-
 src/utils/response.rs       | 24 ++++++++++++++
 src/v1/auth.rs              | 58 +++++++++++++---------------------
 src/v1/posts.rs             | 62 +++++++++++++++++++++++++++++++------
 7 files changed, 116 insertions(+), 62 deletions(-)
 create mode 100644 src/utils/response.rs

diff --git a/src/database/models/post.rs b/src/database/models/post.rs
index 179c1ac..ba80839 100644
--- a/src/database/models/post.rs
+++ b/src/database/models/post.rs
@@ -9,9 +9,10 @@ pub struct Post {
     pub author_id: i32
 }
 
-#[derive(Insertable)]
+#[derive(Insertable, Serialize, Deserialize)]
 #[diesel(table_name = crate::schema::posts)]
-pub struct NewPost<'a> {
-    pub title: &'a str,
-    pub content: &'a str,
+pub struct NewPost {
+    pub title: String,
+    pub content: String,
+    pub author_id: i32
 }
\ No newline at end of file
diff --git a/src/database/models/user.rs b/src/database/models/user.rs
index a862c4d..7b60a19 100644
--- a/src/database/models/user.rs
+++ b/src/database/models/user.rs
@@ -14,9 +14,9 @@ pub struct User {
     pub updated_at: Option<NaiveDateTime>
 }
 
-#[derive(Insertable)]
+#[derive(Insertable, Serialize, Deserialize)]
 #[diesel(table_name = crate::schema::users)]
-pub struct NewUser<'a> {
-    pub username: &'a str,
-    pub password: &'a str
+pub struct NewUser {
+    pub username: String,
+    pub password: String
 }
\ No newline at end of file
diff --git a/src/utils/jwt.rs b/src/utils/jwt.rs
index 0b1a20a..cb9ac5f 100644
--- a/src/utils/jwt.rs
+++ b/src/utils/jwt.rs
@@ -15,17 +15,17 @@ static WEEK: i64 = 60 * 60 * 24 * 7;
 
 #[derive(Debug, Serialize, Deserialize)]
 pub struct Claims {
-    pub sub: String,
+    pub user_id: i32,
     pub exp: usize,
 }
 
 impl Claims {
-    pub fn generate_token(secret: &str, id: &str) -> String {
+    pub fn generate_token(secret: &str, user_id: i32) -> String {
         let now = Utc::now().timestamp();
 
         let payload = Claims {
             exp: (now + WEEK) as usize,
-            sub: id.to_string(),
+            user_id,
         };
         
         jsonwebtoken::encode(
@@ -39,7 +39,7 @@ impl Claims {
 #[derive(Deserialize, Debug, Serialize)]
 pub struct AuthorizedUser {
     pub token: String,
-    pub sub: String,
+    pub user_id: i32,
 }
 
 impl FromRequest for AuthorizedUser {
@@ -63,7 +63,7 @@ impl FromRequest for AuthorizedUser {
                 let parts: Vec<&str> = v.split(" ").collect();
 
                 if parts.len() != 2 {
-                    return ready(Err(ErrorBadRequest("Not Authorized")));
+                    return ready(Err(ErrorBadRequest("unauthorized")));
                 }
 
                 let claims = jsonwebtoken::decode::<Claims>(
@@ -73,12 +73,12 @@ impl FromRequest for AuthorizedUser {
                 );
 
                 if claims.is_err() {
-                    return ready(Err(ErrorBadRequest("Not Authorized")));
+                    return ready(Err(ErrorBadRequest("unauthorized")));
                 }
 
                 let claims = claims.unwrap().claims;
                 let authorized_user = AuthorizedUser {
-                    sub: claims.sub,
+                    user_id: claims.user_id,
                     token: v.to_string()
                 };
 
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 6dbefcf..3d7b94a 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -1 +1,2 @@
-pub mod jwt;
\ No newline at end of file
+pub mod jwt;
+pub mod response;
\ No newline at end of file
diff --git a/src/utils/response.rs b/src/utils/response.rs
new file mode 100644
index 0000000..5c86bd4
--- /dev/null
+++ b/src/utils/response.rs
@@ -0,0 +1,24 @@
+use serde::Serialize;
+
+#[derive(Serialize)]
+pub struct Response<T> {
+    pub ok: bool,
+    pub failure: Option<String>,
+    pub result: Option<T>
+}
+
+pub fn failure<T>(failure: T) -> Response<T> where T: ToString {
+    Response {
+        ok: false,
+        result: None,
+        failure: Some(failure.to_string()),
+    }
+}
+
+pub fn success<T>(result: T) -> Response<T> {
+    Response {
+        ok: true,
+        failure: None,
+        result: Some(result)
+    }
+}
\ No newline at end of file
diff --git a/src/v1/auth.rs b/src/v1/auth.rs
index ad326b8..4ab8e3e 100644
--- a/src/v1/auth.rs
+++ b/src/v1/auth.rs
@@ -1,36 +1,24 @@
 use actix_web::{
-    web::{ServiceConfig, Data, Json, scope}, 
-    HttpResponse, Responder, post
+    post,
+    web::{scope, Data, Json, ServiceConfig},
+    HttpResponse, Responder,
 };
 use argon2::verify_encoded;
-use diesel::{ExpressionMethods, QueryDsl, OptionalExtension, RunQueryDsl};
-use serde::{Serialize, Deserialize};
+use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
 
-use crate::{database::models::user::User, utils::jwt::Claims};
-use crate::schema::users::dsl::*;
+use crate::utils::response::{failure, success};
 use crate::State;
-
-#[derive(Serialize, Deserialize)]
-pub struct UserBody {
-    pub username: String,
-    pub password: String
-}
-
-#[derive(Serialize)]
-struct TokenResponse {
-    ok: bool,
-    token: String
-}
+use crate::{database::models::user::NewUser, schema::users::dsl::*};
+use crate::{database::models::user::User, utils::jwt::Claims};
 
 #[post("/login")]
-async fn login(
-    state: Data<State>,
-    login_data: Json<UserBody>,
-) -> impl Responder {
+async fn login(state: Data<State>, login_data: Json<NewUser>) -> impl Responder {
     let mut conn = match state.db.get() {
         Ok(conn) => conn,
-        Err(why) => return HttpResponse::InternalServerError()
-            .body(format!("database connection error: {}", why)),
+        Err(why) => {
+            return HttpResponse::InternalServerError()
+                .body(format!("database connection error: {}", why))
+        }
     };
 
     let secret = &state.config.jwt_secret;
@@ -42,22 +30,20 @@ async fn login(
     match user_result {
         Ok(Some(user)) => match verify_encoded(&user.password, login_data.password.as_bytes()) {
             Ok(true) => {
-                let token = Claims::generate_token(secret.as_str(), &user.id.to_string());
+                let token = Claims::generate_token(secret.as_str(), user.id);
 
-                HttpResponse::Ok().json(TokenResponse {
-                    ok: true,
-                    token
-                })
+                HttpResponse::Ok().json(success(token))
             }
-            Ok(false) => HttpResponse::Unauthorized().body("Invalid username or password"),
-            Err(err) => HttpResponse::InternalServerError().body(format!("Error verifying password: {}", err)),
+            Ok(false) => HttpResponse::Unauthorized().json(failure("invalid username or password")),
+            Err(err) => HttpResponse::InternalServerError()
+                .json(failure(format!("error while verifying password: {}", err))),
         },
-        Ok(None) | Err(_) => HttpResponse::Unauthorized().body("Invalid username or password"),
+        Ok(None) | Err(_) => {
+            HttpResponse::Unauthorized().json(failure("invalid username or password"))
+        }
     }
 }
 
 pub fn init_routes(cfg: &mut ServiceConfig) {
-    cfg.service(scope("/auth")
-        .service(login)
-    );
-}
\ No newline at end of file
+    cfg.service(scope("/auth").service(login));
+}
diff --git a/src/v1/posts.rs b/src/v1/posts.rs
index 4cfb017..50609ad 100644
--- a/src/v1/posts.rs
+++ b/src/v1/posts.rs
@@ -1,17 +1,30 @@
-use actix_web::{web::{ServiceConfig, Data, scope}, HttpResponse, Responder, get};
-use diesel::RunQueryDsl;
+use actix_web::{
+    get,
+    http::StatusCode,
+    put,
+    web::{scope, Data, Json, ServiceConfig},
+    HttpResponse, Responder,
+};
+use diesel::{insert_into, ExpressionMethods, QueryDsl, RunQueryDsl};
 
-use crate::database::models::post::Post;
 use crate::schema::posts::dsl::*;
+use crate::schema::users::dsl::{id as user_id, users};
+use crate::utils::response::failure;
 use crate::State;
+use crate::{
+    database::models::post::{NewPost, Post},
+    database::models::user::User,
+    utils::jwt::AuthorizedUser,
+};
 
 #[get("")]
 async fn get(state: Data<State>) -> impl Responder {
     let mut conn = match state.db.get() {
         Ok(conn) => conn,
-        Err(why) => return HttpResponse
-            ::InternalServerError()
-            .body(format!("database connection error: {}", why)),
+        Err(why) => {
+            return HttpResponse::InternalServerError()
+                .body(format!("database connection error: {}", why))
+        }
     };
 
     match posts.load::<Post>(&mut conn) {
@@ -20,8 +33,37 @@ async fn get(state: Data<State>) -> impl Responder {
     }
 }
 
+#[put("")]
+async fn create_post(
+    state: Data<State>,
+    auth: AuthorizedUser,
+    mut post: Json<NewPost>,
+) -> impl Responder {
+    let mut conn = match state.db.get() {
+        Ok(conn) => conn,
+        Err(why) => {
+            return HttpResponse::InternalServerError()
+                .body(format!("database connection error: {}", why))
+        }
+    };
+
+    let user = users
+        .filter(user_id.eq(auth.user_id))
+        .first::<User>(&mut conn);
+
+    if user.is_err() {
+        return HttpResponse::InternalServerError()
+            .json(failure("cannot retrieve user from JWT token. sorry."));
+    } else {
+        post.author_id = user.unwrap().id;
+    }
+
+    match insert_into(posts).values(post.0).execute(&mut conn) {
+        Ok(_) => HttpResponse::new(StatusCode::NO_CONTENT),
+        Err(why) => HttpResponse::InternalServerError().json(failure(why.to_string())),
+    }
+}
+
 pub fn init_routes(cfg: &mut ServiceConfig) {
-    cfg.service(scope("/posts")
-        .service(get)
-    );
-}
\ No newline at end of file
+    cfg.service(scope("/posts").service(get).service(create_post));
+}