add(v1): PUT /posts

This commit is contained in:
Franek 2025-04-20 20:29:46 +02:00
parent 8a1d0cf0bb
commit 905baba00a
7 changed files with 116 additions and 62 deletions

View File

@ -9,9 +9,10 @@ pub struct Post {
pub author_id: i32 pub author_id: i32
} }
#[derive(Insertable)] #[derive(Insertable, Serialize, Deserialize)]
#[diesel(table_name = crate::schema::posts)] #[diesel(table_name = crate::schema::posts)]
pub struct NewPost<'a> { pub struct NewPost {
pub title: &'a str, pub title: String,
pub content: &'a str, pub content: String,
pub author_id: i32
} }

View File

@ -14,9 +14,9 @@ pub struct User {
pub updated_at: Option<NaiveDateTime> pub updated_at: Option<NaiveDateTime>
} }
#[derive(Insertable)] #[derive(Insertable, Serialize, Deserialize)]
#[diesel(table_name = crate::schema::users)] #[diesel(table_name = crate::schema::users)]
pub struct NewUser<'a> { pub struct NewUser {
pub username: &'a str, pub username: String,
pub password: &'a str pub password: String
} }

View File

@ -15,17 +15,17 @@ static WEEK: i64 = 60 * 60 * 24 * 7;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Claims { pub struct Claims {
pub sub: String, pub user_id: i32,
pub exp: usize, pub exp: usize,
} }
impl Claims { 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 now = Utc::now().timestamp();
let payload = Claims { let payload = Claims {
exp: (now + WEEK) as usize, exp: (now + WEEK) as usize,
sub: id.to_string(), user_id,
}; };
jsonwebtoken::encode( jsonwebtoken::encode(
@ -39,7 +39,7 @@ impl Claims {
#[derive(Deserialize, Debug, Serialize)] #[derive(Deserialize, Debug, Serialize)]
pub struct AuthorizedUser { pub struct AuthorizedUser {
pub token: String, pub token: String,
pub sub: String, pub user_id: i32,
} }
impl FromRequest for AuthorizedUser { impl FromRequest for AuthorizedUser {
@ -63,7 +63,7 @@ impl FromRequest for AuthorizedUser {
let parts: Vec<&str> = v.split(" ").collect(); let parts: Vec<&str> = v.split(" ").collect();
if parts.len() != 2 { if parts.len() != 2 {
return ready(Err(ErrorBadRequest("Not Authorized"))); return ready(Err(ErrorBadRequest("unauthorized")));
} }
let claims = jsonwebtoken::decode::<Claims>( let claims = jsonwebtoken::decode::<Claims>(
@ -73,12 +73,12 @@ impl FromRequest for AuthorizedUser {
); );
if claims.is_err() { if claims.is_err() {
return ready(Err(ErrorBadRequest("Not Authorized"))); return ready(Err(ErrorBadRequest("unauthorized")));
} }
let claims = claims.unwrap().claims; let claims = claims.unwrap().claims;
let authorized_user = AuthorizedUser { let authorized_user = AuthorizedUser {
sub: claims.sub, user_id: claims.user_id,
token: v.to_string() token: v.to_string()
}; };

View File

@ -1 +1,2 @@
pub mod jwt; pub mod jwt;
pub mod response;

24
src/utils/response.rs Normal file
View File

@ -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)
}
}

View File

@ -1,36 +1,24 @@
use actix_web::{ use actix_web::{
web::{ServiceConfig, Data, Json, scope}, post,
HttpResponse, Responder, post web::{scope, Data, Json, ServiceConfig},
HttpResponse, Responder,
}; };
use argon2::verify_encoded; use argon2::verify_encoded;
use diesel::{ExpressionMethods, QueryDsl, OptionalExtension, RunQueryDsl}; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use serde::{Serialize, Deserialize};
use crate::{database::models::user::User, utils::jwt::Claims}; use crate::utils::response::{failure, success};
use crate::schema::users::dsl::*;
use crate::State; use crate::State;
use crate::{database::models::user::NewUser, schema::users::dsl::*};
#[derive(Serialize, Deserialize)] use crate::{database::models::user::User, utils::jwt::Claims};
pub struct UserBody {
pub username: String,
pub password: String
}
#[derive(Serialize)]
struct TokenResponse {
ok: bool,
token: String
}
#[post("/login")] #[post("/login")]
async fn login( async fn login(state: Data<State>, login_data: Json<NewUser>) -> impl Responder {
state: Data<State>,
login_data: Json<UserBody>,
) -> impl Responder {
let mut conn = match state.db.get() { let mut conn = match state.db.get() {
Ok(conn) => conn, Ok(conn) => conn,
Err(why) => return HttpResponse::InternalServerError() Err(why) => {
.body(format!("database connection error: {}", why)), return HttpResponse::InternalServerError()
.body(format!("database connection error: {}", why))
}
}; };
let secret = &state.config.jwt_secret; let secret = &state.config.jwt_secret;
@ -42,22 +30,20 @@ async fn login(
match user_result { match user_result {
Ok(Some(user)) => match verify_encoded(&user.password, login_data.password.as_bytes()) { Ok(Some(user)) => match verify_encoded(&user.password, login_data.password.as_bytes()) {
Ok(true) => { 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 { HttpResponse::Ok().json(success(token))
ok: true,
token
})
} }
Ok(false) => HttpResponse::Unauthorized().body("Invalid username or password"), Ok(false) => HttpResponse::Unauthorized().json(failure("invalid username or password")),
Err(err) => HttpResponse::InternalServerError().body(format!("Error verifying password: {}", err)), 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) { pub fn init_routes(cfg: &mut ServiceConfig) {
cfg.service(scope("/auth") cfg.service(scope("/auth").service(login));
.service(login) }
);
}

View File

@ -1,17 +1,30 @@
use actix_web::{web::{ServiceConfig, Data, scope}, HttpResponse, Responder, get}; use actix_web::{
use diesel::RunQueryDsl; 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::posts::dsl::*;
use crate::schema::users::dsl::{id as user_id, users};
use crate::utils::response::failure;
use crate::State; use crate::State;
use crate::{
database::models::post::{NewPost, Post},
database::models::user::User,
utils::jwt::AuthorizedUser,
};
#[get("")] #[get("")]
async fn get(state: Data<State>) -> impl Responder { async fn get(state: Data<State>) -> impl Responder {
let mut conn = match state.db.get() { let mut conn = match state.db.get() {
Ok(conn) => conn, Ok(conn) => conn,
Err(why) => return HttpResponse Err(why) => {
::InternalServerError() return HttpResponse::InternalServerError()
.body(format!("database connection error: {}", why)), .body(format!("database connection error: {}", why))
}
}; };
match posts.load::<Post>(&mut conn) { 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) { pub fn init_routes(cfg: &mut ServiceConfig) {
cfg.service(scope("/posts") cfg.service(scope("/posts").service(get).service(create_post));
.service(get) }
);
}