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 } -#[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::( @@ -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 { + pub ok: bool, + pub failure: Option, + pub result: Option +} + +pub fn failure(failure: T) -> Response where T: ToString { + Response { + ok: false, + result: None, + failure: Some(failure.to_string()), + } +} + +pub fn success(result: T) -> Response { + 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, - login_data: Json, -) -> impl Responder { +async fn login(state: Data, login_data: Json) -> 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) -> 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::(&mut conn) { @@ -20,8 +33,37 @@ async fn get(state: Data) -> impl Responder { } } +#[put("")] +async fn create_post( + state: Data, + auth: AuthorizedUser, + mut post: Json, +) -> 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::(&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)); +}