feat(v1): {Post,User}.FromRequest and some new fields
This commit is contained in:
parent
905baba00a
commit
4a6e38c9ab
@ -2,7 +2,11 @@ CREATE TABLE posts (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
title VARCHAR NOT NULL,
|
title VARCHAR NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
author_id INTEGER NOT NULL
|
author_id INTEGER NOT NULL,
|
||||||
|
tags TEXT[] NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
last_updated_at INTEGER
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_author_id ON posts(author_id);
|
CREATE INDEX idx_author_id ON posts(author_id);
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use actix_web::{web::Data, FromRequest};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use crate::{schema::posts::dsl::{id as schema_post_id, posts}, State};
|
||||||
|
|
||||||
#[derive(Queryable, Serialize, Deserialize)]
|
#[derive(Queryable, Serialize, Deserialize)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub author_id: i32
|
pub author_id: i32,
|
||||||
|
pub tags: Vec<Option<String>>,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
pub last_updated_by: Option<i32>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, Serialize, Deserialize)]
|
#[derive(Insertable, Serialize, Deserialize)]
|
||||||
@ -14,5 +24,62 @@ pub struct Post {
|
|||||||
pub struct NewPost {
|
pub struct NewPost {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub author_id: i32
|
pub author_id: i32,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, AsChangeset)]
|
||||||
|
#[diesel(table_name = crate::schema::posts)]
|
||||||
|
pub struct EditPost {
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for Post {
|
||||||
|
type Error = actix_web::error::Error;
|
||||||
|
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||||
|
|
||||||
|
fn from_request(
|
||||||
|
req: &actix_web::HttpRequest,
|
||||||
|
_: &mut actix_web::dev::Payload,
|
||||||
|
) -> Self::Future {
|
||||||
|
let req = req.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let state = req.app_data::<Data<State>>();
|
||||||
|
if state.is_none() {
|
||||||
|
return Err(actix_web::error::ErrorInternalServerError(
|
||||||
|
"cannot retrieve app state, therefore can not retrieve post data",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = state.unwrap();
|
||||||
|
let mut conn = match state.db.get() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(..) => Err(actix_web::error::ErrorInternalServerError(
|
||||||
|
"cannot retrieve database connection from the pool",
|
||||||
|
))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let post_id = actix_web::web::Path::<i32>::extract(&req).await;
|
||||||
|
match post_id {
|
||||||
|
Ok(id) => {
|
||||||
|
let post = posts
|
||||||
|
.filter(schema_post_id.eq(id.into_inner()))
|
||||||
|
.first::<Post>(&mut conn)
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
match post {
|
||||||
|
Ok(Some(post)) => Ok(post),
|
||||||
|
Ok(None) => Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"post with this ID is not found",
|
||||||
|
)),
|
||||||
|
Err(why) => Err(actix_web::error::ErrorInternalServerError(why)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(why) => Err(actix_web::error::ErrorInternalServerError(why)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ use serde::{Serialize, Deserialize};
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
#[derive(Queryable, Serialize, Deserialize)]
|
#[derive(Queryable, Serialize, Deserialize, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
@ -6,6 +6,10 @@ diesel::table! {
|
|||||||
title -> Varchar,
|
title -> Varchar,
|
||||||
content -> Text,
|
content -> Text,
|
||||||
author_id -> Int4,
|
author_id -> Int4,
|
||||||
|
tags -> Array<Nullable<Text>>,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
last_updated_by -> Nullable<Int4>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
use actix_web::{
|
use actix_web::{
|
||||||
error::{ErrorBadRequest, ErrorInternalServerError},
|
dev::Payload,
|
||||||
web::Data, HttpRequest, dev::Payload,
|
error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound},
|
||||||
FromRequest, Error
|
web::Data,
|
||||||
|
Error, FromRequest, HttpRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
use jsonwebtoken::{Header, EncodingKey, DecodingKey, Validation};
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use std::future::{self, ready};
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::future::{self, ready};
|
||||||
|
|
||||||
use crate::State;
|
use crate::schema::users::dsl::{id as schema_user_id, users};
|
||||||
|
use crate::{database::models::user::User, State};
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
static WEEK: i64 = 60 * 60 * 24 * 7;
|
static WEEK: i64 = 60 * 60 * 24 * 7;
|
||||||
|
|
||||||
@ -31,8 +34,9 @@ impl Claims {
|
|||||||
jsonwebtoken::encode(
|
jsonwebtoken::encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
&payload,
|
&payload,
|
||||||
&EncodingKey::from_secret(secret.as_ref())
|
&EncodingKey::from_secret(secret.as_ref()),
|
||||||
).unwrap()
|
)
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +44,23 @@ impl Claims {
|
|||||||
pub struct AuthorizedUser {
|
pub struct AuthorizedUser {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_user(state: &Data<State>, user_id: i32) -> Option<User> {
|
||||||
|
let mut conn = match state.db.get() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(..) => None?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = users
|
||||||
|
.filter(schema_user_id.eq(user_id))
|
||||||
|
.first::<User>(&mut conn);
|
||||||
|
|
||||||
|
match user {
|
||||||
|
Ok(user) => Some(user),
|
||||||
|
Err(..) => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRequest for AuthorizedUser {
|
impl FromRequest for AuthorizedUser {
|
||||||
@ -50,11 +71,11 @@ impl FromRequest for AuthorizedUser {
|
|||||||
let state = req.app_data::<Data<State>>();
|
let state = req.app_data::<Data<State>>();
|
||||||
let header = req.headers().get("Authorization");
|
let header = req.headers().get("Authorization");
|
||||||
if header.is_none() {
|
if header.is_none() {
|
||||||
return future::ready(Err(ErrorBadRequest("no Authorization header specified")))
|
return future::ready(Err(ErrorBadRequest("no Authorization header specified")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.is_none() {
|
if state.is_none() {
|
||||||
return future::ready(Err(ErrorInternalServerError("cannot retrieve JWT token")))
|
return future::ready(Err(ErrorInternalServerError("cannot retrieve JWT token")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = state.unwrap();
|
let state = state.unwrap();
|
||||||
@ -69,7 +90,7 @@ impl FromRequest for AuthorizedUser {
|
|||||||
let claims = jsonwebtoken::decode::<Claims>(
|
let claims = jsonwebtoken::decode::<Claims>(
|
||||||
parts[1],
|
parts[1],
|
||||||
&DecodingKey::from_secret(state.config.jwt_secret.as_ref()),
|
&DecodingKey::from_secret(state.config.jwt_secret.as_ref()),
|
||||||
&Validation::default()
|
&Validation::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if claims.is_err() {
|
if claims.is_err() {
|
||||||
@ -77,12 +98,22 @@ impl FromRequest for AuthorizedUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let claims = claims.unwrap().claims;
|
let claims = claims.unwrap().claims;
|
||||||
let authorized_user = AuthorizedUser {
|
let user = get_user(state, claims.user_id);
|
||||||
user_id: claims.user_id,
|
|
||||||
token: v.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
ready(Ok(authorized_user))
|
match user {
|
||||||
|
Some(user) => {
|
||||||
|
let authorized_user = AuthorizedUser {
|
||||||
|
user_id: claims.user_id,
|
||||||
|
token: v.to_string(),
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
|
||||||
|
ready(Ok(authorized_user))
|
||||||
|
}
|
||||||
|
None => ready(Err(ErrorNotFound(
|
||||||
|
"invalid login credentials, user not found",
|
||||||
|
))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => ready(Err(ErrorBadRequest(e))),
|
Err(e) => ready(Err(ErrorBadRequest(e))),
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
use actix_web::{
|
use actix_web::{
|
||||||
get,
|
get,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
put,
|
patch, put,
|
||||||
web::{scope, Data, Json, ServiceConfig},
|
web::{scope, Data, Json, ServiceConfig},
|
||||||
HttpResponse, Responder,
|
HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use diesel::{insert_into, ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{insert_into, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
use crate::schema::posts::dsl::*;
|
|
||||||
use crate::schema::users::dsl::{id as user_id, users};
|
|
||||||
use crate::utils::response::failure;
|
use crate::utils::response::failure;
|
||||||
use crate::State;
|
use crate::State;
|
||||||
|
use crate::{database::models::post::EditPost, schema::posts::dsl as posts_dsl};
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::post::{NewPost, Post},
|
database::models::post::{NewPost, Post},
|
||||||
database::models::user::User,
|
|
||||||
utils::jwt::AuthorizedUser,
|
utils::jwt::AuthorizedUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,7 +25,7 @@ async fn get(state: Data<State>) -> impl Responder {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match posts.load::<Post>(&mut conn) {
|
match posts_dsl::posts.load::<Post>(&mut conn) {
|
||||||
Ok(data) => HttpResponse::Ok().json(data),
|
Ok(data) => HttpResponse::Ok().json(data),
|
||||||
Err(why) => HttpResponse::InternalServerError().body(why.to_string()),
|
Err(why) => HttpResponse::InternalServerError().body(why.to_string()),
|
||||||
}
|
}
|
||||||
@ -37,7 +35,7 @@ async fn get(state: Data<State>) -> impl Responder {
|
|||||||
async fn create_post(
|
async fn create_post(
|
||||||
state: Data<State>,
|
state: Data<State>,
|
||||||
auth: AuthorizedUser,
|
auth: AuthorizedUser,
|
||||||
mut post: Json<NewPost>,
|
post: Json<NewPost>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let mut conn = match state.db.get() {
|
let mut conn = match state.db.get() {
|
||||||
Ok(conn) => conn,
|
Ok(conn) => conn,
|
||||||
@ -47,18 +45,39 @@ async fn create_post(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let user = users
|
match insert_into(posts_dsl::posts)
|
||||||
.filter(user_id.eq(auth.user_id))
|
.values((post.0, posts_dsl::author_id.eq(auth.user_id)))
|
||||||
.first::<User>(&mut conn);
|
.execute(&mut conn)
|
||||||
|
{
|
||||||
if user.is_err() {
|
Ok(_) => HttpResponse::new(StatusCode::NO_CONTENT),
|
||||||
return HttpResponse::InternalServerError()
|
Err(why) => HttpResponse::InternalServerError().json(failure(why.to_string())),
|
||||||
.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) {
|
#[patch("/{id}")]
|
||||||
|
async fn update_post(
|
||||||
|
state: Data<State>,
|
||||||
|
auth: AuthorizedUser,
|
||||||
|
post: Post,
|
||||||
|
new_post: Json<EditPost>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let mut conn = match state.db.get() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(why) => {
|
||||||
|
return HttpResponse::InternalServerError()
|
||||||
|
.body(format!("database connection error: {}", why))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Security, anti-abuse checks
|
||||||
|
match diesel::update(posts_dsl::posts.filter(posts_dsl::id.eq(post.id)))
|
||||||
|
.set((
|
||||||
|
new_post.0,
|
||||||
|
posts_dsl::updated_at.eq(diesel::dsl::now),
|
||||||
|
posts_dsl::last_updated_by.eq(auth.user_id),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
{
|
||||||
Ok(_) => HttpResponse::new(StatusCode::NO_CONTENT),
|
Ok(_) => HttpResponse::new(StatusCode::NO_CONTENT),
|
||||||
Err(why) => HttpResponse::InternalServerError().json(failure(why.to_string())),
|
Err(why) => HttpResponse::InternalServerError().json(failure(why.to_string())),
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user