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
}
#[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
}

View File

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

View File

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

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::{
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)
);
}
cfg.service(scope("/auth").service(login));
}

View File

@ -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)
);
}
cfg.service(scope("/posts").service(get).service(create_post));
}