feat: refactor a lot

This commit is contained in:
Kilerd Chan 2019-06-03 16:29:18 +08:00
parent fe49f97e4e
commit f8a8bf1148
20 changed files with 612 additions and 318 deletions

16
Cargo.lock generated
View File

@ -930,6 +930,20 @@ name = "itoa"
version = "0.4.3" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jsonwebtoken"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
@ -1626,6 +1640,7 @@ dependencies = [
"dotenv 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)",
"http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.10.22 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.10.22 (registry+https://github.com/rust-lang/crates.io-index)",
@ -2428,6 +2443,7 @@ dependencies = [
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
"checksum ipconfig 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7b78ac2cfe6655f29ccc510ce0500c8356c3756dbce759d0ecb8a3718f2ef3" "checksum ipconfig 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7b78ac2cfe6655f29ccc510ce0500c8356c3756dbce759d0ecb8a3718f2ef3"
"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
"checksum jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a81d1812d731546d2614737bee92aa071d37e9afa1409bc374da9e5e70e70b22"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"

View File

@ -39,3 +39,4 @@ time = "0.1.42"
rand = "0.6.5" rand = "0.6.5"
rss = "1.7.0" rss = "1.7.0"
actix = "0.8.3" actix = "0.8.3"
jsonwebtoken = "6.0.1"

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE "public"."articles" ALTER COLUMN "publish_at" SET DEFAULT NOW();

View File

@ -12,7 +12,7 @@ use actix_web::{
middleware::{ middleware::{
cors::Cors, cors::Cors,
identity::{CookieIdentityPolicy, Identity, IdentityService}, identity::{CookieIdentityPolicy, Identity, IdentityService},
Logger, Logger, NormalizePath,
}, },
web, App, HttpServer, web, App, HttpServer,
}; };
@ -32,12 +32,13 @@ mod models;
mod pg_pool; mod pg_pool;
mod routers; mod routers;
mod schema; mod schema;
mod utils;
mod view; mod view;
embed_migrations!(); embed_migrations!();
lazy_static! { lazy_static! {
static ref RANDOM_TOKEN_KEY: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect(); static ref RANDOM_TOKEN_KEY: Vec<u8> = (0..32).map(|_| 0).collect();
} }
fn main() { fn main() {
@ -62,33 +63,14 @@ fn main() {
.data(FormConfig::default().limit(256_000)) .data(FormConfig::default().limit(256_000))
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(Cors::default()) .wrap(Cors::default())
.wrap(NormalizePath)
.wrap(IdentityService::new( .wrap(IdentityService::new(
CookieIdentityPolicy::new(&RANDOM_TOKEN_KEY) CookieIdentityPolicy::new(&RANDOM_TOKEN_KEY)
.name("auth-cookie") .name("auth-cookie")
.secure(false) .secure(false)
.max_age_time(Duration::days(3)), .max_age_time(Duration::days(3)),
)) ))
.service(routers::article::homepage) .service(routers::routes())
.service(routers::article::single_article)
.service(actix_files::Files::new(
"/statics",
"./templates/resources/",
))
.service(routers::admin::redirect_to_admin_panel)
.service(
web::scope("/admin/")
.service(routers::admin::admin_panel)
.service(routers::admin::admin_login)
.service(routers::admin::admin_authentication)
.service(routers::admin::article_creation)
.service(routers::admin::article_save)
.service(routers::admin::article_edit)
.service(routers::admin::article_deletion)
.service(routers::admin::change_password)
.service(routers::admin::change_setting),
)
.service(routers::rss::rss_page)
.service(routers::article::get_article_by_url)
}) })
.bind(("0.0.0.0", 8000)) .bind(("0.0.0.0", 8000))
.unwrap() .unwrap()

View File

@ -20,52 +20,19 @@ pub struct Article {
pub url: Option<String>, pub url: Option<String>,
pub keywords: Vec<String>, pub keywords: Vec<String>,
} }
//
#[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)] #[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)]
#[table_name = "articles"] #[table_name = "articles"]
pub struct NewArticle { pub struct NewArticle {
pub id: Option<i32>,
pub title: String, pub title: String,
pub body: String, pub body: String,
pub published: bool, pub published: bool,
pub user_id: i32, pub user_id: i32,
pub publish_at: NaiveDateTime, pub publish_at: Option<NaiveDateTime>,
pub url: Option<String>, pub url: Option<String>,
pub keywords: Vec<String>, pub keywords: Vec<String>,
} }
pub mod form {
use crate::models::article::NewArticle;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct NewArticleForm {
pub id: Option<i32>,
pub title: String,
pub body: String,
pub published: bool,
pub user_id: i32,
pub publish_at: NaiveDateTime,
pub url: Option<String>,
pub keywords: String,
}
impl Into<NewArticle> for NewArticleForm {
fn into(self) -> NewArticle {
NewArticle {
id: self.id,
title: self.title,
body: self.body,
published: self.published,
user_id: self.user_id,
publish_at: self.publish_at,
url: self.url,
keywords: self.keywords.split(",").map(String::from).collect(),
}
}
}
}
impl Article { impl Article {
pub fn link(&self) -> String { pub fn link(&self) -> String {
match self.url { match self.url {

View File

@ -3,8 +3,8 @@ use diesel::result::Error;
pub mod article; pub mod article;
pub mod setting; pub mod setting;
pub mod token;
pub mod user; pub mod user;
pub trait CRUD<CreatedModel, UpdateModel, PK> { pub trait CRUD<CreatedModel, UpdateModel, PK> {
fn create(conn: &PgConnection, from: &CreatedModel) -> Result<Self, Error> fn create(conn: &PgConnection, from: &CreatedModel) -> Result<Self, Error>
where where

View File

@ -21,8 +21,14 @@ pub struct SettingMap {
pub url: String, pub url: String,
pub analysis: String, pub analysis: String,
} }
#[derive(Queryable, Debug, Serialize, Deserialize, AsChangeset)]
#[table_name = "setting"]
pub struct UpdateSetting {
pub value: Option<String>,
}
impl Setting { impl Setting {
// TODO refactor this method
pub fn load(conn: &PgConnection) -> SettingMap { pub fn load(conn: &PgConnection) -> SettingMap {
let settings = setting::table.load::<Setting>(conn).unwrap(); let settings = setting::table.load::<Setting>(conn).unwrap();
@ -51,7 +57,7 @@ impl Setting {
} }
} }
impl CRUD<(), Setting, String> for Setting { impl CRUD<(), UpdateSetting, String> for Setting {
fn create(conn: &PgConnection, from: &()) -> Result<Self, Error> { fn create(conn: &PgConnection, from: &()) -> Result<Self, Error> {
unimplemented!() unimplemented!()
} }
@ -60,7 +66,7 @@ impl CRUD<(), Setting, String> for Setting {
unimplemented!() unimplemented!()
} }
fn update(conn: &PgConnection, pk: String, value: &Setting) -> Result<Self, Error> { fn update(conn: &PgConnection, pk: String, value: &UpdateSetting) -> Result<Self, Error> {
diesel::update(setting::table.find(&pk)) diesel::update(setting::table.find(&pk))
.set(value) .set(value)
.get_result(conn) .get_result(conn)

6
src/models/token.rs Normal file
View File

@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Token {
pub token: String,
}

View File

@ -11,6 +11,12 @@ use diesel::prelude::*;
use diesel::{AsChangeset, Insertable, Queryable}; use diesel::{AsChangeset, Insertable, Queryable};
use serde::Serialize; use serde::Serialize;
use crate::data::RubbleData;
use crate::utils::jwt::JWTClaims;
use actix_web::dev::Payload;
use actix_web::{error, FromRequest, HttpRequest};
use futures::IntoFuture;
#[derive(Queryable, Debug, Serialize, Insertable, AsChangeset)] #[derive(Queryable, Debug, Serialize, Insertable, AsChangeset)]
#[table_name = "users"] #[table_name = "users"]
pub struct User { pub struct User {
@ -65,3 +71,41 @@ impl CRUD<(), User, i32> for User {
users::table.filter(users::id.eq(pk)).first::<User>(conn) users::table.filter(users::id.eq(pk)).first::<User>(conn)
} }
} }
impl FromRequest for User {
type Error = actix_web::error::Error;
type Future = Result<Self, Self::Error>;
type Config = ();
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let tokens: Vec<&str> = req
.headers()
.get("Authorization")
.ok_or(error::ErrorUnauthorized("cannot find authorization header"))?
.to_str()
.map_err(|_| error::ErrorBadRequest("error on deserialize token"))?
.splitn(2, ' ')
.collect();
let user_id = JWTClaims::decode(tokens[1].into())
.map_err(|_| error::ErrorUnauthorized("invalid jwt token"))?;
let data = req
.app_data::<RubbleData>()
.ok_or(error::ErrorBadGateway("error on get rubble data"))?;
let result = User::find_by_username(&data.postgres(), &user_id)
.map_err(|_| error::ErrorUnauthorized("error on get user"))?;
Ok(result)
}
}
pub mod input {
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct LoginForm {
pub username: String,
pub password: String,
}
}

View File

@ -1,224 +1,241 @@
use actix_web::middleware::identity::Identity; //use actix_web::middleware::identity::Identity;
use actix_web::web::Form; //use actix_web::web::Form;
use actix_web::{get, post, web, Either, HttpResponse, Responder}; //use actix_web::{get, post, web, Either, HttpResponse, Responder};
use chrono::{NaiveDateTime, Utc}; //use chrono::{NaiveDateTime, Utc};
use serde::Deserialize; //use serde::{Deserialize, Serialize};
use tera::{Context, Tera}; //use tera::{Context, Tera};
//
//use crate::data::RubbleData;
//use crate::models::article::{Article, NewArticle};
//use crate::models::setting::Setting;
//use crate::models::token::Token;
//use crate::models::user::User;
//use crate::models::CRUD;
//use crate::routers::RubbleResponder;
//
//use crate::utils::jwt::JWTClaims;
//
//#[derive(Deserialize, Serialize)]
//pub struct LoginForm {
// pub username: String,
// pub password: String,
//}
//
//#[derive(Deserialize)]
//pub struct NewPassword {
// password: String,
//}
//
//#[get("/admin")]
//pub fn redirect_to_admin_panel() -> RubbleResponder<()> {
// RubbleResponder::Redirect {
// to: "/admin/panel".into(),
// }
//}
//
use crate::data::RubbleData; //
use crate::models::article::{Article, NewArticle}; //#[get("/login")]
use crate::models::setting::Setting; //pub fn admin_login(id: Identity, data: web::Data<RubbleData>) -> RubbleResponder<()> {
use crate::models::user::User; // match id.identity() {
use crate::models::CRUD; // Some(_) => RubbleResponder::Redirect {
use crate::routers::RubbleResponder; // to: "/admin/panel".into(),
// },
#[derive(Deserialize)] // None => RubbleResponder::Html(data.render("admin/login.html", &Context::new())),
pub struct LoginForm { // }
pub username: String, //}
pub password: String, //
} //#[post("/login")]
//pub fn admin_authentication(
#[derive(Deserialize)] // id: Identity,
pub struct NewPassword { // user: Form<LoginForm>,
password: String, // data: web::Data<RubbleData>,
} //) -> RubbleResponder<()> {
// let fetched_user = User::find_by_username(&data.postgres(), &user.username);
#[get("/admin")] //
pub fn redirect_to_admin_panel() -> impl Responder { // match fetched_user {
RubbleResponder::Redirect("/admin/panel".into()) // Ok(login_user) => {
} // if login_user.authenticated(&user.password) {
// id.remember(login_user.username);
#[get("/panel")] // info!("admin login");
pub fn admin_panel(id: Identity, data: web::Data<RubbleData>) -> impl Responder { // RubbleResponder::Redirect {
if id.identity().is_none() { // to: "/admin/panel".into(),
return RubbleResponder::Redirect("/admin/login".into()); // }
} // } else {
// // TODO flash message or throw unauthorized
let articles = Article::read(&data.postgres()); // warn!("try logining admin with wrong password '{}'", user.password);
let settings = Setting::load(&data.postgres()); // RubbleResponder::Redirect {
// to: "/admin/login".into(),
let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap()) // }
.expect("cannot found this user"); // }
// }
let mut context = Context::new(); // Err(_) => RubbleResponder::Redirect {
context.insert("setting", &settings); // to: "/admin/login".into(),
context.insert("articles", &articles); // },
context.insert("admin", &admin); // }
//}
RubbleResponder::Html(data.render("admin/panel.html", &context)) //
} //#[get("/article/new")]
//pub fn article_creation(id: Identity, data: web::Data<RubbleData>) -> RubbleResponder<()> {
#[get("/login")] // if id.identity().is_none() {
pub fn admin_login(id: Identity, data: web::Data<RubbleData>) -> impl Responder { // return RubbleResponder::Redirect {
match id.identity() { // to: "/admin/login".into(),
Some(_) => RubbleResponder::Redirect("/admin/panel".into()), // };
None => RubbleResponder::Html(data.render("admin/login.html", &Context::new())), // }
} //
} // let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap())
// .expect("cannot found this user");
#[post("/login")] //
pub fn admin_authentication( // let mut context = Context::new();
id: Identity, //
user: Form<LoginForm>, // let article = NewArticle {
data: web::Data<RubbleData>, // id: None,
) -> impl Responder { // title: String::new(),
let fetched_user = User::find_by_username(&data.postgres(), &user.username); // body: String::new(),
// published: true,
match fetched_user { // user_id: admin.id,
Ok(login_user) => { // publish_at: NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0),
if login_user.authenticated(&user.password) { // url: None,
id.remember(login_user.username); // keywords: vec![],
info!("admin login"); // };
RubbleResponder::Redirect("/admin/panel".into()) //
} else { // context.insert("article", &article);
// TODO flash message or throw unauthorized // RubbleResponder::Html(data.render("admin/article_add.html", &context))
warn!("try logining admin with wrong password '{}'", user.password); //}
RubbleResponder::Redirect("/admin/login".into()) //
} //#[get("/article/{article_id}")]
} //pub fn article_edit(
Err(_) => RubbleResponder::Redirect("/admin/login".into()), // id: Identity,
} // article_id: web::Path<i32>,
} // data: web::Data<RubbleData>,
//) -> RubbleResponder<()> {
#[get("/article/new")] // if id.identity().is_none() {
pub fn article_creation(id: Identity, data: web::Data<RubbleData>) -> impl Responder { // return RubbleResponder::Redirect {
if id.identity().is_none() { // to: "/admin/login".into(),
return RubbleResponder::Redirect("/admin/login".into()); // };
} // }
//
let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap()) // let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap())
.expect("cannot found this user"); // .expect("cannot found this user");
//
let mut context = Context::new(); // let result = Article::get_by_pk(&data.postgres(), article_id.into_inner());
//
let article = NewArticle { // match result {
id: None, // Ok(article) => {
title: String::new(), // let mut context = Context::new();
body: String::new(), // context.insert("article", &article);
published: true, // RubbleResponder::Html(data.render("admin/article_add.html", &context))
user_id: admin.id, // }
publish_at: NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0), // Err(_) => RubbleResponder::Redirect {
url: None, // to: "/admin/panel".into(),
keywords: vec![], // },
}; // }
//}
context.insert("article", &article); //
RubbleResponder::Html(data.render("admin/article_add.html", &context)) //#[post("/article")]
} //pub fn article_save(
// id: Identity,
#[get("/article/{article_id}")] // article: Form<crate::models::article::form::NewArticleForm>,
pub fn article_edit( // data: web::Data<RubbleData>,
id: Identity, //) -> RubbleResponder<()> {
article_id: web::Path<i32>, // if id.identity().is_none() {
data: web::Data<RubbleData>, // return RubbleResponder::Redirect {
) -> impl Responder { // to: "/admin/login".into(),
if id.identity().is_none() { // };
return RubbleResponder::Redirect("/admin/login".into()); // }
} //
// let article_title = article.title.clone();
let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap()) //
.expect("cannot found this user"); // let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap())
// .expect("cannot found this user");
let result = Article::get_by_pk(&data.postgres(), article_id.into_inner()); // let res = if let Some(article_id) = article.id {
// info!("updating article #{} {}", article_id, article_title);
match result { // Article::update(&data.postgres(), article_id, &article.into_inner().into())
Ok(article) => { // } else {
let mut context = Context::new(); // info!("creating new article {}", article_title);
context.insert("article", &article); // Article::create(&data.postgres(), &article.into_inner().into())
RubbleResponder::Html(data.render("admin/article_add.html", &context)) // };
} //
Err(_) => RubbleResponder::Redirect("/admin/panel".into()), // if res.is_err() {
} // error!("error on updating/creating article {}", article_title);
} // }
// RubbleResponder::Redirect {
#[post("/article")] // to: "/admin/panel".into(),
pub fn article_save( // }
id: Identity, //}
article: Form<crate::models::article::form::NewArticleForm>, //
data: web::Data<RubbleData>, //#[post("/article/delete/{article_id}")]
) -> impl Responder { //pub fn article_deletion(
if id.identity().is_none() { // id: Identity,
return RubbleResponder::Redirect("/admin/login".into()); // article_id: web::Path<i32>,
} // data: web::Data<RubbleData>,
//) -> RubbleResponder<()> {
let article_title = article.title.clone(); // if id.identity().is_none() {
// return RubbleResponder::Redirect {
let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap()) // to: "/admin/login".into(),
.expect("cannot found this user"); // };
let res = if let Some(article_id) = article.id { // }
info!("updating article #{} {}", article_id, article_title); //
Article::update(&data.postgres(), article_id, &article.into_inner().into()) // let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap())
} else { // .expect("cannot found this user");
info!("creating new article {}", article_title); //
Article::create(&data.postgres(), &article.into_inner().into()) // let i = article_id.into_inner();
}; // Article::delete(&data.postgres(), i);
// info!("deleting article {}", i);
if res.is_err() { // RubbleResponder::Redirect {
error!("error on updating/creating article {}", article_title); // to: "/admin/panel".into(),
} // }
RubbleResponder::Redirect("/admin/panel".into()) //}
} //
//#[post("/password")]
#[post("/article/delete/{article_id}")] //pub fn change_password(
pub fn article_deletion( // id: Identity,
id: Identity, // password: web::Form<NewPassword>,
article_id: web::Path<i32>, // data: web::Data<RubbleData>,
data: web::Data<RubbleData>, //) -> RubbleResponder<()> {
) -> impl Responder { // if id.identity().is_none() {
if id.identity().is_none() { // return RubbleResponder::Redirect {
return RubbleResponder::Redirect("/admin/login".into()); // to: "/admin/login".into(),
} // };
// }
let admin = User::find_by_username(&data.postgres(), &id.identity().unwrap()) //
.expect("cannot found this user"); // let mut admin = User::find_by_username(&data.postgres(), &id.identity().unwrap())
// .expect("cannot found this user");
let i = article_id.into_inner(); // admin.password = User::password_generate(&password.password).to_string();
Article::delete(&data.postgres(), i); // User::update(&data.postgres(), admin.id, &admin);
info!("deleting article {}", i); // id.forget();
RubbleResponder::Redirect("/admin/panel".into()) // info!("updating password");
} // RubbleResponder::Redirect {
// to: "/admin/panel".into(),
#[post("/password")] // }
pub fn change_password( //}
id: Identity, //
password: web::Form<NewPassword>, //#[post("/setting")]
data: web::Data<RubbleData>, //pub fn change_setting(
) -> impl Responder { // id: Identity,
if id.identity().is_none() { // setting: web::Form<Setting>,
return RubbleResponder::Redirect("/admin/login".into()); // data: web::Data<RubbleData>,
} //) -> RubbleResponder<()> {
// if id.identity().is_none() {
let mut admin = User::find_by_username(&data.postgres(), &id.identity().unwrap()) // return RubbleResponder::Redirect {
.expect("cannot found this user"); // to: "/admin/login".into(),
admin.password = User::password_generate(&password.password).to_string(); // };
User::update(&data.postgres(), admin.id, &admin); // }
id.forget(); //
info!("updating password"); // let mut admin = User::find_by_username(&data.postgres(), &id.identity().unwrap())
RubbleResponder::Redirect("/admin/panel".into()) // .expect("cannot found this user");
} //
// Setting::update(&data.postgres(), setting.name.clone(), &setting);
#[post("/setting")] // info!("updating setting {:?} to {:?}", setting.name, setting.value);
pub fn change_setting( // RubbleResponder::Redirect {
id: Identity, // to: "/admin/panel".into(),
setting: web::Form<Setting>, // }
data: web::Data<RubbleData>, //}
) -> impl Responder { //
if id.identity().is_none() { //#[cfg(test)]
return RubbleResponder::Redirect("/admin/login".into()); //mod test {
} // #[test]
// fn test_normal() {
let mut admin = User::find_by_username(&data.postgres(), &id.identity().unwrap()) // assert_eq!(1, 1);
.expect("cannot found this user"); // }
//}
Setting::update(&data.postgres(), setting.name.clone(), &setting);
info!("updating setting {:?} to {:?}", setting.name, setting.value);
RubbleResponder::Redirect("/admin/panel".into())
}
#[cfg(test)]
mod test {
#[test]
fn test_normal() {
assert_eq!(1, 1);
}
}

View File

@ -0,0 +1,56 @@
use crate::{
data::RubbleData,
models::{article::Article, user::User, CRUD},
routers::RubbleResponder,
};
use actix_web::{delete, get, post, put, web, Responder};
#[get("")]
pub fn get_all_article(user: User, data: web::Data<RubbleData>) -> impl Responder {
RubbleResponder::json(Article::read(&data.postgres()))
}
#[get("/{id}")]
pub fn get_all_article_by_id(
user: User,
id: web::Path<i32>,
data: web::Data<RubbleData>,
) -> impl Responder {
Article::get_by_pk(&data.postgres(), *id)
.map(|data| RubbleResponder::json(data))
.map_err(|_| RubbleResponder::not_found())
}
#[post("")]
pub fn crate_article(
user: User,
article: web::Json<crate::models::article::NewArticle>,
data: web::Data<RubbleData>,
) -> impl Responder {
Article::create(&data.postgres(), &article)
.map(RubbleResponder::json)
.map_err(|_| RubbleResponder::bad_request("something wrong when creating article"))
}
#[put("/{id}")]
pub fn update_article_by_id(
user: User,
id: web::Path<i32>,
article: web::Json<crate::models::article::NewArticle>,
data: web::Data<RubbleData>,
) -> impl Responder {
Article::update(&data.postgres(), *id, &article)
.map(|data| RubbleResponder::json(data))
.map_err(|_| RubbleResponder::bad_request("something wrong when updating article"))
}
#[delete("/{id}")]
pub fn delete_article_by_id(
user: User,
id: web::Path<i32>,
data: web::Data<RubbleData>,
) -> impl Responder {
Article::delete(&data.postgres(), *id)
.map(|_| RubbleResponder::json("Ok"))
.map_err(|_| RubbleResponder::bad_request("something wrong when deleting article"))
}

3
src/routers/api/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod article;
pub mod setting;
pub mod user;

View File

@ -0,0 +1,28 @@
use crate::{
data::RubbleData,
models::{
setting::{Setting, UpdateSetting},
user::User,
CRUD,
},
routers::RubbleResponder,
};
use actix_web::{delete, get, post, put, web, Responder};
#[get("")]
pub fn get_settings(user: User, data: web::Data<RubbleData>) -> impl Responder {
RubbleResponder::json(Setting::load(&data.postgres()))
}
#[put("/{key}")]
pub fn update_setting_by_key(
user: User,
key: web::Path<String>,
value: web::Json<UpdateSetting>,
data: web::Data<RubbleData>,
) -> impl Responder {
let string = (*key).clone();
Setting::update(&data.postgres(), string, &value)
.map(RubbleResponder::json)
.map_err(|_| RubbleResponder::bad_request("error on updating setting"))
}

58
src/routers/api/user.rs Normal file
View File

@ -0,0 +1,58 @@
//! /authentications routes
use crate::{
data::RubbleData,
models::{
token::Token,
user::{input::LoginForm, User},
},
routers::RubbleResponder,
utils::jwt::JWTClaims,
};
use actix_web::{delete, get, post, put, web, Responder};
#[post("/token")]
pub fn admin_authentication(
user: web::Json<LoginForm>,
data: web::Data<RubbleData>,
) -> impl Responder {
let fetched_user = User::find_by_username(&data.postgres(), &user.username);
match fetched_user {
Ok(login_user) => {
if login_user.authenticated(&user.password) {
let string = JWTClaims::encode(&login_user);
RubbleResponder::json(Token { token: string })
} else {
RubbleResponder::unauthorized("invalid password")
}
}
Err(_) => RubbleResponder::unauthorized("invalid username"),
}
}
#[get("")]
pub fn get_all_users() -> impl Responder {
unreachable!()
}
#[post("")]
pub fn crate_user() -> impl Responder {
unreachable!()
}
#[put("/{id}")]
pub fn update_user_by_id() -> impl Responder {
unreachable!()
}
#[delete("/{id}")]
pub fn delete_user_by_id() -> impl Responder {
unreachable!()
}
#[put("/{id}/password")]
pub fn update_user_password() -> impl Responder {
unreachable!()
}

View File

@ -6,10 +6,11 @@ use crate::pg_pool::Pool;
use crate::routers::RubbleResponder; use crate::routers::RubbleResponder;
use crate::view::article::ArticleView; use crate::view::article::ArticleView;
use actix_web::{get, web, Either, HttpResponse, Responder}; use actix_web::{get, web, Either, HttpResponse, Responder};
use std::result::Result;
use std::sync::Arc; use std::sync::Arc;
use tera::{Context, Tera}; use tera::{Context, Tera};
#[get("/")] #[get("")]
pub fn homepage(data: web::Data<RubbleData>) -> impl Responder { pub fn homepage(data: web::Data<RubbleData>) -> impl Responder {
let vec: Vec<Article> = Article::read(&data.postgres()); let vec: Vec<Article> = Article::read(&data.postgres());
let article_view: Vec<_> = vec let article_view: Vec<_> = vec
@ -24,21 +25,21 @@ pub fn homepage(data: web::Data<RubbleData>) -> impl Responder {
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("articles", &article_view); context.insert("articles", &article_view);
RubbleResponder::Html(data.render("homepage.html", &context)) RubbleResponder::html(data.render("homepage.html", &context))
} }
#[get("/archives/{archives_id}")] #[get("archives/{archives_id}")]
pub fn single_article(archives_id: web::Path<i32>, data: web::Data<RubbleData>) -> impl Responder { pub fn single_article(archives_id: web::Path<i32>, data: web::Data<RubbleData>) -> impl Responder {
let article = Article::get_by_pk(&data.postgres(), archives_id.into_inner()); let article = Article::get_by_pk(&data.postgres(), archives_id.into_inner());
if let Err(e) = article { if let Err(e) = article {
return RubbleResponder::NotFound; return RubbleResponder::not_found();
} }
let article1 = article.unwrap(); let article1 = article.unwrap();
if let Some(ref to) = article1.url { if let Some(ref to) = article1.url {
if to.len() != 0 { if to.len() != 0 {
return RubbleResponder::Redirect(format!("/{}", to)); return RubbleResponder::redirect(format!("/{}", to));
} }
} }
@ -50,15 +51,15 @@ pub fn single_article(archives_id: web::Path<i32>, data: web::Data<RubbleData>)
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("article", &view); context.insert("article", &view);
RubbleResponder::Html(data.render("archives.html", &context)) RubbleResponder::html(data.render("archives.html", &context))
} }
#[get("/{url}")] #[get("{url}")]
pub fn get_article_by_url(url: web::Path<String>, data: web::Data<RubbleData>) -> impl Responder { pub fn get_article_by_url(url: web::Path<String>, data: web::Data<RubbleData>) -> impl Responder {
let article = Article::find_by_url(&data.postgres(), &url.into_inner()); let article = Article::find_by_url(&data.postgres(), &url.into_inner());
if let Err(e) = article { if let Err(e) = article {
return RubbleResponder::NotFound; return RubbleResponder::not_found();
} }
let article1 = article.unwrap(); let article1 = article.unwrap();
@ -70,5 +71,5 @@ pub fn get_article_by_url(url: web::Path<String>, data: web::Data<RubbleData>) -
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("article", &view); context.insert("article", &view);
RubbleResponder::Html(data.render("archives.html", &context)) RubbleResponder::html(data.render("archives.html", &context))
} }

View File

@ -1,35 +1,97 @@
use actix_web::{HttpRequest, HttpResponse, Responder}; use actix_web::{HttpRequest, HttpResponse, Responder};
pub mod admin; use actix_web::{error::Error, web, Scope};
pub mod article;
use actix_web::error::Error;
use futures::future::{err, ok, FutureResult}; use futures::future::{err, ok, FutureResult};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
pub mod admin;
pub mod api;
pub mod article;
pub mod rss; pub mod rss;
pub enum RubbleResponder { #[derive(Deserialize, Serialize)]
Html(String), pub struct JsonResponse<T> {
Redirect(String), data: T,
NotFound,
RedirectPermanently(String),
} }
impl Responder for RubbleResponder { #[derive(Deserialize, Serialize)]
type Error = Error; pub struct ErrorResponse<T> {
type Future = FutureResult<HttpResponse, Error>; message: T,
}
fn respond_to(self, req: &HttpRequest) -> Self::Future { pub struct RubbleResponder;
match self {
RubbleResponder::Html(content) => ok(HttpResponse::Ok() impl RubbleResponder {
pub fn html(content: impl Into<String>) -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")
.body(content)), .body(content.into())
RubbleResponder::Redirect(to) => ok(HttpResponse::Found()
.header(http::header::LOCATION, to)
.finish()),
RubbleResponder::NotFound => ok(HttpResponse::NotFound().finish()),
RubbleResponder::RedirectPermanently(to) => ok(HttpResponse::MovedPermanently()
.header(http::header::LOCATION, to)
.finish()),
} }
pub fn json(data: impl Serialize) -> HttpResponse {
HttpResponse::Ok()
.header(
http::header::CONTENT_TYPE,
"application/json; charset=utf-8",
)
.json(JsonResponse { data })
}
pub fn text(content: impl Into<String>) -> HttpResponse {
HttpResponse::Ok().body(content.into())
}
pub fn redirect(to: impl Into<String>) -> HttpResponse {
HttpResponse::Found()
.header(http::header::LOCATION, to.into())
.finish()
}
pub fn redirect_permanently(to: impl Into<String>) -> HttpResponse {
HttpResponse::MovedPermanently()
.header(http::header::LOCATION, to.into())
.finish()
}
pub fn not_found() -> HttpResponse {
HttpResponse::NotFound().finish()
}
pub fn unauthorized(reason: impl Serialize) -> HttpResponse {
HttpResponse::Unauthorized().json(&ErrorResponse { message: reason })
}
pub fn bad_gateway(reason: impl Serialize) -> HttpResponse {
HttpResponse::BadGateway().json(&ErrorResponse { message: reason })
}
pub fn bad_request(reason: impl Serialize) -> HttpResponse {
HttpResponse::BadRequest().json(&ErrorResponse { message: reason })
} }
} }
pub fn routes() -> Scope {
web::scope("/")
.service(
web::scope("/api")
.service(web::scope("/users").service(api::user::admin_authentication))
.service(
web::scope("/articles")
.service(api::article::get_all_article)
.service(api::article::get_all_article_by_id)
.service(api::article::crate_article)
.service(api::article::update_article_by_id)
.service(api::article::delete_article_by_id),
)
.service(
web::scope("/settings")
.service(api::setting::get_settings)
.service(api::setting::update_setting_by_key),
),
)
.service(article::homepage)
.service(article::single_article)
.service(actix_files::Files::new(
"/statics",
"./templates/resources/",
))
.service(rss::rss_)
.service(article::get_article_by_url)
}

View File

@ -7,12 +7,13 @@ use rss::{Channel, ChannelBuilder, Item, ItemBuilder};
use std::collections::HashMap; use std::collections::HashMap;
#[get("/rss")] #[get("/rss")]
pub fn rss_page(data: web::Data<RubbleData>) -> impl Responder { pub fn rss_(data: web::Data<RubbleData>) -> impl Responder {
let articles = Article::read(&data.postgres()); let articles = Article::read(&data.postgres());
let setting = Setting::load(&data.postgres()); let setting = Setting::load(&data.postgres());
let items: Vec<Item> = articles let items: Vec<Item> = articles
.iter() .iter()
.filter(|article| article.published == true)
.map(ArticleView::from) .map(ArticleView::from)
.map(|item| { .map(|item| {
ItemBuilder::default() ItemBuilder::default()

42
src/utils/jwt.rs Normal file
View File

@ -0,0 +1,42 @@
use crate::models::user::User;
use crate::RANDOM_TOKEN_KEY;
use chrono::prelude::*;
use jsonwebtoken::{decode as jwt_decode, encode as jwt_encode, Algorithm, Header, Validation};
use serde::{Deserialize, Serialize};
use std::ops::Add;
use time::Duration;
#[derive(Debug, Serialize, Deserialize)]
pub struct JWTClaims {
iat: usize,
sub: String,
exp: usize,
id: i32,
username: String,
}
impl JWTClaims {
pub fn encode(user: &User) -> String {
let now: DateTime<Utc> = Utc::now();
let expire: DateTime<Utc> = Utc::now().add(Duration::days(7));
let claims = JWTClaims {
iat: now.timestamp() as usize,
sub: String::from("LOGIN_TOKEN"),
exp: expire.timestamp() as usize,
id: user.id,
username: user.username.clone(),
};
jwt_encode(&Header::default(), &claims, &RANDOM_TOKEN_KEY).unwrap()
}
pub fn decode(token: String) -> Result<String, ()> {
let claims = jwt_decode::<JWTClaims>(
token.as_str(),
&RANDOM_TOKEN_KEY,
&Validation::new(Algorithm::HS256),
)
.map_err(|e| ())?;
Ok(claims.claims.username)
}
}

1
src/utils/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod jwt;